* [Tarantool-patches] [PATCH luajit 0/3] Extend debug extension
@ 2026-06-25 20:29 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
` (2 more replies)
0 siblings, 3 replies; 6+ messages in thread
From: Sergey Kaplun via Tarantool-patches @ 2026-06-25 20:29 UTC (permalink / raw)
To: Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches
This patchset adds the commands to process FFI-related and JIT-related
components of LuaJIT. The last patch simplifies the testing process with
verbose mode by environment variable.
Branch: https://github.com/tarantool/luajit/tree/skaplun/gh-4808-jslots-trace-ir-cdata-ctype
Related issue: https://github.com/tarantool/tarantool/issues/4808
Sergey Kaplun (3):
dbg: introduce lj-ir, lj-jslots, lj-trace dumpers
dbg: introduce lj-ctype command, extend cdata dump
test: add verbose mode for debug extension tests
src/luajit_dbg.py | 1547 ++++++++++++++++-
.../debug-extension-tests.py | 580 +++++-
2 files changed, 2111 insertions(+), 16 deletions(-)
--
2.54.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [Tarantool-patches] [PATCH luajit 1/3] dbg: introduce lj-ir, lj-jslots, lj-trace dumpers 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 2026-06-28 1:03 ` Evgeniy Temirgaleev via Tarantool-patches 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 2 siblings, 1 reply; 6+ messages in thread From: Sergey Kaplun via Tarantool-patches @ 2026-06-25 20:29 UTC (permalink / raw) To: Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches 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 ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [Tarantool-patches] [PATCH luajit 1/3] dbg: introduce lj-ir, lj-jslots, lj-trace dumpers 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 0 siblings, 0 replies; 6+ messages in thread From: Evgeniy Temirgaleev via Tarantool-patches @ 2026-06-28 1:03 UTC (permalink / raw) To: Sergey Kaplun; +Cc: tarantool-patches [-- 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 --] ^ permalink raw reply [flat|nested] 6+ messages in thread
* [Tarantool-patches] [PATCH luajit 2/3] dbg: introduce lj-ctype command, extend cdata dump 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-25 20:29 ` 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 2 siblings, 0 replies; 6+ messages in thread From: Sergey Kaplun via Tarantool-patches @ 2026-06-25 20:29 UTC (permalink / raw) To: Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches This patch extends dumped information for the given cdata object. Now it resolves the given `CType` and prints it in the format similar to the `__tostring` metamethod. The `lj-ctype` command is introduced to dump this information where there is only the `CType` pointer but no cdata associated with it. `__or__` and `__ror__` metamethods are monkey-patched for the LLDB value object. In `__sub__` metamethod for LLDB pointers `GetPointeeType()` is used to get the pointee type instead of the incorrectly used `GetDereferencedType()` which always returns the same type with size 8. Casting from negative values to the unsigned values is supported to check `CTF_UCHAR`. Part of tarantool/tarantool#4808 --- src/luajit_dbg.py | 333 +++++++++++++++++- .../debug-extension-tests.py | 208 ++++++++++- 2 files changed, 535 insertions(+), 6 deletions(-) diff --git a/src/luajit_dbg.py b/src/luajit_dbg.py index fd6ca8a5..62cd65d5 100644 --- a/src/luajit_dbg.py +++ b/src/luajit_dbg.py @@ -386,6 +386,8 @@ class _LLDBDebugger(Debugger): pack_flag = '<q' else: pack_flag = '<Q' + # Cast to unsigned. + raw_value &= 0xFFFFFFFFFFFFFFFF raw_data = struct.pack(pack_flag, raw_value) sbdata = lldb.SBData() sbdata.SetData( @@ -482,6 +484,9 @@ class _LLDBDebugger(Debugger): def lldb__lt__(lldbval, other): return int(lldbval) < int(other) + def lldb__or__(lldbval, other): + return int(lldbval) | int(other) + def lldb__str__(lldbval): # Instead of default GetSummary. if not lldbval.sbvalue.TypeIsPointerType(): @@ -512,8 +517,8 @@ class _LLDBDebugger(Debugger): lldbval_tp = sbval.GetType() other_tp = osbval.GetType() # Subtract pointers of the same size only. - elsz = lldbval_tp.GetDereferencedType().size - if other_tp.GetDereferencedType().size != elsz: + elsz = lldbval_tp.GetPointeeType().size + if other_tp.GetPointeeType().size != elsz: raise Exception( 'Attempt to substruct {otp} from {stp}'.format( stp=lldbval_tp.name, @@ -536,6 +541,8 @@ class _LLDBDebugger(Debugger): lldb.value.__index__ = lldb__index__ lldb.value.__le__ = lldb__le__ lldb.value.__lt__ = lldb__lt__ + lldb.value.__or__ = lldb__or__ + lldb.value.__ror__ = lldb__or__ # Same semantics. lldb.value.__str__ = lldb__str__ lldb.value.__sub__ = lldb__sub__ @@ -1352,6 +1359,119 @@ def lightudV(tv): return gcval(tv['gcr']) +# FFI. + + +def ctype_ctsG(g): + return mref('CTState *', g['ctype_state']) + + +def ctype_get(cts, id): + return dbg.address(cts['tab'][id]) + + +# Externally visible types. +CT_NUM = 0 # Integer or floating-point numbers. +CT_STRUCT = 1 # Struct or union. +CT_PTR = 2 # Pointer or reference. +CT_ARRAY = 3 # Array or complex type. +CT_MAYCONVERT = CT_ARRAY +CT_VOID = 4 # Void type. +CT_ENUM = 5 # Enumeration. +CT_HASSIZE = CT_ENUM # Last type where ct->size holds the actual size. +CT_FUNC = 6 # Function. +CT_TYPEDEF = 7 # Typedef. +CT_ATTRIB = 8 # Miscellaneous attributes. + +# Common types. +CTID_CTYPEID = 21 + +# C type info flags. +CTF_BOOL = 0x08000000 # Boolean: NUM, BITFIELD. +CTF_FP = 0x04000000 # Floating-point: NUM. +CTF_CONST = 0x02000000 # Const qualifier. +CTF_VOLATILE = 0x01000000 # Volatile qualifier. +CTF_UNSIGNED = 0x00800000 # Unsigned: NUM, BITFIELD. +CTF_LONG = 0x00400000 # Long: NUM. +CTF_VLA = 0x00100000 # Variable-length: ARRAY, STRUCT. +CTF_REF = 0x00800000 # Reference: PTR. +CTF_VECTOR = 0x08000000 # Vector: ARRAY. +CTF_COMPLEX = 0x04000000 # Complex: ARRAY. +CTF_UNION = 0x00800000 # Union: STRUCT. +CTF_VARARG = 0x00800000 # Vararg: FUNC. +CTF_SSEREGPARM = 0x00400000 # SSE register parameters: FUNC. + +CTF_UCHAR = CTF_UNSIGNED if int(dbg.cast('char', -1)) > 0 else 0 + +CTMASK_ATTRIB = 255 # Max. 256 attributes. +CTSHIFT_ATTRIB = 16 + +# Attribute numbers. +CTA_QUAL = 1 # Unmerged qualifiers. + +CTSHIFT_NUM = 28 +CTMASK_CID = 0x0000ffff +CTMASK_NUM = 0xf0000000 # Max. 16 type numbers. + +# Special sizes. +CTSIZE_INVALID = 0xffffffff +DWORDSZ = 4 +QWORDSZ = 8 + + +def ctype_type(info): + return info >> CTSHIFT_NUM + + +def ctype_attrib(info): + return (info >> CTSHIFT_ATTRIB) & CTMASK_ATTRIB + + +def ctinfo(ct, flags): + return (tou32(ct) << CTSHIFT_NUM) + flags + + +def ctype_isptr(info): + return ctype_type(info) == CT_PTR + + +def ctype_iscomplex(info): + return (info & (CTMASK_NUM | CTF_COMPLEX)) == ctinfo(CT_ARRAY, CTF_COMPLEX) + + +def ctype_isinteger(info): + return (info & (CTMASK_NUM | CTF_BOOL | CTF_FP)) == ctinfo(CT_NUM, 0) + + +def ctype_isrefarray(info): + return (info & (CTMASK_NUM | CTF_VECTOR | CTF_COMPLEX)) == \ + ctinfo(CT_ARRAY, 0) + + +def ctype_cid(info): + return info & CTMASK_CID + + +def ctype_child(cts, ctype): + return ctype_get(cts, ctype_cid(ctype['info'])) + + +def cdataptr(cd): + return dbg.cast('void *', (cd + 1)) + + +def cdata_getptr(p, size): + if LJ_64 and size == 4: + return dbg.cast('void *', dbg.cast('uint32_t *', p)[0]) + else: + return dbg.cast('void *', dbg.cast('uint64_t *', p)[0]) + + +# Get C type ID for a C type. +def ctype_typeid(cts, ct): + return ct - cts['tab'] + + # JIT engine. @@ -1951,7 +2071,26 @@ def dump_lj_gco_trace(gcobj): def dump_lj_gco_cdata(gcobj): - return 'cdata @ {}'.format(strx64(gcobj)) + cdata = dbg.cast('struct GCcdata *', gcobj) + cts = ctype_ctsG(G(L())) + cid = cdata['ctypeid'] + ctype = ctype_get(cts, cid) + info = ctype['info'] + size = ctype['size'] + value = '' + if ctype_iscomplex(info): + value = cdata_val_complex(cdata, ctype) + elif size == 8 and ctype_isinteger(info): + value = cdata_val_int64(cdata, ctype) + else: + value = cdataptr(cdata) + if ctype_isptr(info): + value = cdata_getptr(value, size) + return 'cdata @ {addr} {ctype} {value}'.format( + addr=strx64(gcobj), + ctype=dump_ctype(ctype), + value=value, + ) def dump_lj_gco_tab(gcobj): @@ -2281,6 +2420,176 @@ def dump_func(func): return 'fast function #{}\n'.format(int(ffid)) +# FFI dumpers. + + +def cdata_val_int64(cdata, ctype): + info = ctype['info'] + isunsigned = info & CTF_UNSIGNED + cdataval = cdataptr(cdata) + valueptr = None + usuffix = '' + if isunsigned: + usuffix = 'U' + valueptr = dbg.cast('uint64_t *', cdataval) + else: + valueptr = dbg.cast('int64_t *', cdataval) + return str(valueptr[0]) + usuffix + 'LL' + + +def cdata_val_complex(cdata, ctype): + size = ctype['size'] + cdataval = cdataptr(cdata) + casttype = None + if size == QWORDSZ * 2: + casttype = 'double *' + else: + assert size == DWORDSZ * 2, 'bad (complex float) size' + casttype = 'float *' + re = dbg.cast(casttype, cdataval)[0] + im = dbg.cast(casttype, cdataval)[1] + sign = '+' if im > 0 else '' + return '{re}{sign}{im}i'.format(re=re, im=im, sign=sign) + + +def ctype_preplit(ctypestr, lit): + # Prevent extra space in the end of the string. + space = ' ' if ctypestr != '' else '' + return lit + space + ctypestr + + +def ctype_prepqual(ctypestr, info): + if (info & CTF_VOLATILE): + ctypestr = ctype_preplit(ctypestr, 'volatile') + if (info & CTF_CONST): + ctypestr = ctype_preplit(ctypestr, 'const') + return ctypestr + + +def ctype_preptype(cts, ctypestr, ctype, qual, tp): + nameref = gcref(ctype['name']) + if nameref: + ctypestr = ctype_preplit(ctypestr, re.sub('"', '', strdata(nameref))) + else: + ctypestr = ctype_preplit(ctypestr, str(ctype_typeid(cts, ctype))) + ctypestr = ctype_preplit(ctypestr, tp) + ctypestr = ctype_prepqual(ctypestr, qual) + return ctypestr + + +def ctype_prepnum(ctypestr, info, size): + if info & CTF_BOOL: + ctypestr = ctype_preplit(ctypestr, 'bool') + elif info & CTF_FP: + if size == QWORDSZ: + ctypestr = ctype_preplit(ctypestr, 'double') + elif size == DWORDSZ: + ctypestr = ctype_preplit(ctypestr, 'float') + else: + assert size == QWORDSZ * 2, 'bad (long double) size' + ctypestr = ctype_preplit(ctypestr, 'long double') + elif size == 1: + if not ((info ^ CTF_UCHAR) & CTF_UNSIGNED): + ctypestr = ctype_preplit(ctypestr, 'char') + elif CTF_UCHAR: + ctypestr = ctype_preplit(ctypestr, 'signed char') + else: + ctypestr = ctype_preplit(ctypestr, 'unsigned char') + elif size < 8: + if size == 4: + ctypestr = ctype_preplit(ctypestr, 'int') + else: + assert size == DWORDSZ // 2, 'bad (short) size' + ctypestr = ctype_preplit(ctypestr, 'short') + if info & CTF_UNSIGNED: + ctypestr = ctype_preplit(ctypestr, 'unsigned') + else: + size_t = '{u}int{sz}_t'.format( + u='u' if info & CTF_UNSIGNED else '', + sz=size * 8, + ) + ctypestr = ctype_preplit(ctypestr, size_t) + return ctypestr + + +def ctype_repr(cts, id): + ctype = ctype_get(cts, id) + ctypestr = '' + qual = 0 + ptrto = 0 + while True: + info = ctype['info'] + size = ctype['size'] + ctp = ctype_type(info) + if ctp == CT_NUM: + ctypestr = ctype_prepnum(ctypestr, info, size) + return ctype_prepqual(ctypestr, qual | info) + elif ctp == CT_VOID: + ctypestr = ctype_preplit(ctypestr, 'void') + return ctype_prepqual(ctypestr, qual | info) + elif ctp == CT_STRUCT: + tp = 'union' if (info & CTF_UNION) else 'struct' + return ctype_preptype(cts, ctypestr, ctype, qual, tp) + elif ctp == CT_ENUM: + if id == CTID_CTYPEID: + return ctype_preplit(ctypestr, 'ctype') + return ctype_preptype(cts, ctypestr, ctype, qual, 'enum') + elif ctp == CT_ATTRIB: + if ctype_attrib(info) == CTA_QUAL: + qual |= size + elif ctp == CT_PTR: + if info & CTF_REF: + ctypestr = ctype_preplit(ctypestr, '&') + else: + ctypestr = ctype_prepqual(ctypestr, qual | info) + if LJ_64 and size == 4: + ctypestr = ctype_preplit(ctypestr, '__ptr32') + ctypestr = ctype_preplit(ctypestr, '*') + qual = 0 + ptrto = 1 + elif ctp == CT_ARRAY: + if ctype_isrefarray(info): + if ptrto: + ptrto = 0 + ctypestr = '(' + ctypestr + ')' + arrsize = '' + if size != CTSIZE_INVALID: + child_size = ctype_child(cts, ctype)['size'] + arrsize = str(int(size / child_size) if child_size > 0 + else 0) + elif info & CTF_VLA: + arrsize = '?' + ctypestr = ctypestr + '[{}]'.format(arrsize) + elif ctype_iscomplex(info): + if size == DWORDSZ * 2: + ctypestr = ctype_preplit(ctypestr, 'float') + else: + assert size == QWORDSZ * 2, 'bad (complex double) size' + return ctype_preplit(ctypestr, 'complex') + else: + ctypestr = ctype_preplit( + ctypestr, + '__attribute__((vector_size({})))'.format(size) + ) + elif ctp == CT_FUNC: + if ptrto: + ptrto = 0 + ctypestr = '(' + ctypestr + ')' + ctypestr += '()' + ctype = ctype_child(cts, ctype) + return 'NYI' + + +def dump_ctype(ct): + cts = ctype_ctsG(G(L())) + cid = ctype_typeid(cts, ct) + name = ctype_repr(cts, cid) + return '[{id}] <{name}>'.format( + id=cid, + name=name, + ) + + # JIT dumpers. @@ -2294,7 +2603,8 @@ def dump_call_func(trace, callop): assert IRS[cdt_idx_irk['o']] == 'KINT', \ 'unexpected IR for ctype storage' ctype_idx = cdt_idx_irk['i'] - ctype = 'ctype: {}'.format(ctype_idx) + cts = ctype_ctsG(G(L())) + ctype = 'ctype: {}'.format(dump_ctype(ctype_get(cts, ctype_idx))) func_str = '' if callop < 0: @@ -2652,6 +2962,20 @@ https://github.com/tarantool/tarantool/wiki/LuaJIT-Bytecodes. )) +class LJDumpCType(dbg.LJBase): + ''' +lj-ctype <CType *> + +The command receives a pointer <ctype> of the corresponding CType +and dumps the ID and the name for this C data type. + ''' + + def execute(self, arg): + dbg.write('{}\n'.format( + dump_ctype(dbg.cast('CType *', dbg.eval(arg))) + )) + + class LJDumpFunc(dbg.LJBase): ''' lj-func <GCfunc *> @@ -2979,6 +3303,7 @@ def load(event=None): dbg.initialize_extension({ 'lj-arch': LJDumpArch, 'lj-bc': LJDumpBC, + 'lj-ctype': LJDumpCType, 'lj-func': LJDumpFunc, 'lj-gc': LJGC, 'lj-gco': LJDumpGCobj, diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py index 76543daa..fc5d2c7b 100644 --- a/test/tarantool-debugger-tests/debug-extension-tests.py +++ b/test/tarantool-debugger-tests/debug-extension-tests.py @@ -227,6 +227,7 @@ class TestLoad(TestCaseBase): pattern = ( r'lj-arch command initialized\n' r'lj-bc command initialized\n' + r'lj-ctype command initialized\n' r'lj-func command initialized\n' r'lj-gc command initialized\n' r'lj-gco command initialized\n' @@ -331,7 +332,7 @@ GCO_RX = ( r'Lua function @ ' + RX_ADDR + r', [0-9]+ upvalues, .+:[0-9]+\n' r'C function @ ' + RX_ADDR + r'\n' r'fast function #[0-9]+\n' - r'cdata @ ' + RX_ADDR + r'\n' + r'cdata @ ' + RX_ADDR + r' \[\d+\] <int \*> 0x0\n' r'table @ ' + RX_ADDR + r' \(asize: \d+, hmask: ' + RX_HASH + r'\)\n' r'userdata @ ' + RX_ADDR + r'\n' ) @@ -817,7 +818,9 @@ class TestLJIRCallXSCType(TestCaseBase): 'trace()\n' 'print()\n' ) - pattern = r'int CALLXS .* [' + RX_ADDR + r'\]\(.*\) ctype: \d+' + pattern = ( + r'int CALLXS .* [' + RX_ADDR + r'\]\(.*\) ctype: \[\d+\] <int \(\)>' + ) class TestLJJSlotsBase(TestCaseBase): @@ -838,6 +841,207 @@ class TestLJJSlotsBase(TestCaseBase): ) +def cdata_rx(tpstr, suffix=None): + return r'cdata @ ' + RX_ADDR + r' \[\d+\] <' + tpstr + '> ' + ( + RX_ADDR if not suffix else suffix + ) + + +CHAR_SIGNED = machine in ['arm64', 'aarch64'] and sys.platform != 'darwin' +HAS_LONG_DOUBLE = not (machine in ['arm64', 'aarch64'] and + sys.platform == 'darwin') + + +class TestLJCTypePrim(TestCaseBase): + location = 'lj_cf_print' + extension_cmds = ( + 'n\n' # Load L. + 'lj-tv L->base\n' + 'lj-tv L->base + 1\n' + 'lj-tv L->base + 2\n' + 'lj-tv L->base + 3\n' + 'lj-tv L->base + 4\n' + 'lj-tv L->base + 5\n' + 'lj-tv L->base + 6\n' + 'lj-tv L->base + 7\n' + 'lj-tv L->base + 8\n' + 'lj-tv L->base + 9\n' + 'lj-tv L->base + 10\n' + 'lj-tv L->base + 11\n' + 'lj-tv L->base + 12\n' + 'lj-tv L->base + 13\n' + 'lj-tv L->base + 14\n' + 'lj-tv L->base + 15\n' + 'lj-tv L->base + 16\n' + 'lj-tv L->base + 17\n' + 'lj-tv L->base + 18\n' + 'lj-tv L->base + 19\n' + 'lj-tv L->base + 20\n' + 'lj-tv L->base + 21\n' + 'lj-tv L->base + 22\n' + ) + lua_script = ( + 'local ffi = require("ffi")\n' + 'print(\n' + ' ffi.new("bool"),\n' + ' ffi.new("char"),\n' + ' ffi.new("signed char"),\n' + ' ffi.new("unsigned char"),\n' + ' ffi.new("int"),\n' + ' ffi.new("short"),\n' + ' ffi.new("unsigned"),\n' + ' ffi.new("int8_t"),\n' + ' ffi.new("int16_t"),\n' + ' ffi.new("int32_t"),\n' + ' ffi.new("int64_t"),\n' + ' ffi.new("uint8_t"),\n' + ' ffi.new("uint64_t"),\n' + ' ffi.new("float"),\n' + ' ffi.new("double"),\n' + ' ffi.new("long double"),\n' + ' 1i,\n' + ' ffi.new("complex float", 1, -2),\n' + ' ffi.new("const volatile int"),\n' + ' ffi.new("void *"),\n' + ' ffi.new("void * __ptr32"),\n' + ' ffi.new("int &"),\n' + ' ffi.typeof(1LL)\n' + ')\n' + ) + pattern = ( + cdata_rx('bool') + r'\n' + + cdata_rx('char') + r'\n' + + cdata_rx(('signed ' if CHAR_SIGNED else '') + 'char') + r'\n' + + cdata_rx(('unsigned ' if not CHAR_SIGNED else '') + 'char') + r'\n' + + cdata_rx('int') + r'\n' + + cdata_rx('short') + r'\n' + + cdata_rx('unsigned int') + r'\n' + + cdata_rx(('signed ' if CHAR_SIGNED else '') + 'char') + r'\n' + + cdata_rx('short') + r'\n' + + cdata_rx('int') + r'\n' + + cdata_rx('int64_t', '0LL') + r'\n' + + cdata_rx(('unsigned ' if not CHAR_SIGNED else '') + 'char') + r'\n' + + cdata_rx('uint64_t', '0ULL') + r'\n' + + cdata_rx('float') + r'\n' + + cdata_rx('double') + r'\n' + + cdata_rx(('long ' if HAS_LONG_DOUBLE else '') + 'double') + r'\n' + + cdata_rx('complex', r'0\+1i') + r'\n' + + cdata_rx('complex float', '1-2i') + r'\n' + + cdata_rx('const volatile int') + r'\n' + + cdata_rx(r'void \*') + r'\n' + + cdata_rx(r'void \* __ptr32') + r'\n' + + cdata_rx('int &') + r'\n' + + cdata_rx('ctype') + r'\n' + ) + + +class TestLJCTypeStructUnionEnum(TestCaseBase): + location = 'lj_cf_print' + extension_cmds = ( + 'n\n' # Load L. + 'lj-tv L->base\n' + 'lj-tv L->base + 1\n' + 'lj-tv L->base + 2\n' + 'lj-tv L->base + 3\n' + ) + lua_script = ( + 'local ffi = require("ffi")\n' + 'ffi.cdef[[\n' + ' struct test {int a;};\n' + ']]\n' + 'print(\n' + ' ffi.new("struct test"),\n' + ' ffi.new("struct {int a;}"),\n' + ' ffi.new("union {int a;}"),\n' + ' ffi.new("enum {ENUM1}")\n' + ')\n' + ) + pattern = ( + cdata_rx('struct test') + r'\n' + + cdata_rx(r'struct \d+') + r'\n' + + cdata_rx(r'union \d+') + r'\n' + + cdata_rx(r'enum \d+') + r'\n' + ) + + +class TestLJCTypeArray(TestCaseBase): + location = 'lj_cf_print' + extension_cmds = ( + 'n\n' # Load L. + 'lj-tv L->base\n' + 'lj-tv L->base + 1\n' + 'lj-tv L->base + 2\n' + 'lj-tv L->base + 3\n' + 'lj-tv L->base + 4\n' + 'lj-tv L->base + 5\n' + 'lj-tv L->base + 6\n' + ) + lua_script = ( + 'local ffi = require("ffi")\n' + 'print(\n' + ' ffi.new("char [0]"),\n' + ' ffi.new("int [1]"),\n' + ' ffi.new("complex [2]"),\n' + ' ffi.new("complex float [3]"),\n' + ' ffi.new("float __attribute__((vector_size(4)))"),\n' + ' ffi.new("int (&)[5]"),\n' + ' ffi.new("int[?]", 6)\n' + ')\n' + ) + pattern = ( + cdata_rx(r'char \[0\]') + r'\n' + + cdata_rx(r'int \[1\]') + r'\n' + + cdata_rx(r'complex \[2\]') + r'\n' + + cdata_rx(r'complex float \[3\]') + r'\n' + + cdata_rx(r'float __attribute__\(\(vector_size\(4\)\)\)') + r'\n' + + cdata_rx(r'int \(&\)\[5\]') + r'\n' + + cdata_rx(r'int \[\?\]') + r'\n' + ) + + +class TestLJCTypeFunc(TestCaseBase): + location = 'lj_cf_print' + extension_cmds = ( + 'n\n' # Load L. + 'lj-tv L->base\n' + 'lj-tv L->base + 1\n' + 'lj-tv L->base + 2\n' + ) + lua_script = ( + 'local ffi = require("ffi")\n' + 'ffi.cdef[[void getpid(void);]]\n' + 'print(\n' + ' ffi.C.getpid,\n' + ' ffi.new("int (*)()"),\n' + ' ffi.new("int (*(*)(void))[2]")\n' + ')\n' + ) + pattern = ( + cdata_rx(r'void \(\)') + r'\n' + + cdata_rx(r'int \(\*\)\(\)') + r'\n' + + cdata_rx(r'int \(\* \(\*\)\(\)\)\[2\]') + r'\n' + ) + + +class TestLJCTypeBase(TestCaseBase): + location = 'lj_cf_ffi_new' + extension_cmds = ( + # Load `ct`. Skip inlined functions for LLDB. + 'n\n' + 'n\n' + 'n\n' + 'n\n' + 'n\n' + 'n\n' + 'lj-ctype ct\n' + ) + lua_script = ( + 'local ffi = require("ffi")\n' + 'ffi.new("int")\n' + ) + pattern = r'\[\d+\] <int>' + + for test_cls in TestCaseBase.__subclasses__(): test_cls.test = lambda self: self.check() -- 2.54.0 ^ permalink raw reply [flat|nested] 6+ messages in thread
* [Tarantool-patches] [PATCH luajit 3/3] test: add verbose mode for debug extension tests 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-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 ` Sergey Kaplun via Tarantool-patches 2026-06-28 1:31 ` Evgeniy Temirgaleev via Tarantool-patches 2 siblings, 1 reply; 6+ messages in thread From: Sergey Kaplun via Tarantool-patches @ 2026-06-25 20:29 UTC (permalink / raw) To: Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches If the environment variable `DUBUGGER_TEST_VERBOSE` is set, each test prints the generated command and its output and doesn't delete the files generated for it. --- test/tarantool-debugger-tests/debug-extension-tests.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py index fc5d2c7b..adb83e1e 100644 --- a/test/tarantool-debugger-tests/debug-extension-tests.py +++ b/test/tarantool-debugger-tests/debug-extension-tests.py @@ -43,6 +43,8 @@ else: # Don't run any initialization scripts. RUN_CMD_FILE = ['--batch', '--nx', '--quiet', '--command'] +TEST_VERBOSE = os.getenv('DUBUGGER_TEST_VERBOSE', default=False) + RX_ADDR = r'0x[a-f0-9]+' RX_HASH = RX_ADDR # The same pattern for hexademic values. RX_BCN = r'00\d\d' @@ -52,7 +54,7 @@ RX_IRREF = r'0x\d\d\d\d' def persist(data): - tmp = tempfile.NamedTemporaryFile(mode='w') + tmp = tempfile.NamedTemporaryFile(mode='w', delete=not TEST_VERBOSE) tmp.write(data) tmp.flush() return tmp @@ -149,7 +151,12 @@ class TestCaseBase(unittest.TestCase): LUAJIT_BINARY, script_file.name, ] + if TEST_VERBOSE: + print('# Test name: {}'.format(cls.__name__)) + print('# Test command: {}'.format(' '.join(process_cmd))) cls.output = execute_process(process_cmd) + if TEST_VERBOSE: + print('# Command output: {}'.format(cls.output)) cmd_file.close() script_file.close() -- 2.54.0 ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [Tarantool-patches] [PATCH luajit 3/3] test: add verbose mode for debug extension tests 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 0 siblings, 0 replies; 6+ messages in thread From: Evgeniy Temirgaleev via Tarantool-patches @ 2026-06-28 1:31 UTC (permalink / raw) To: Sergey Kaplun; +Cc: tarantool-patches [-- Attachment #1: Type: text/plain, Size: 2065 bytes --] Hi, Sergey! Thanks for the patch! LGTM after fixing the 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 > If the environment variable `DUBUGGER_TEST_VERBOSE` is set, each test > prints the generated command and its output and doesn't delete the files > generated for it. > --- > test/tarantool-debugger-tests/debug-extension-tests.py | 9 ++++++++- > 1 file changed, 8 insertions(+), 1 deletion(-) > > diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py > b/test/tarantool-debugger-tests/debug-extension-tests.py > index fc5d2c7b..adb83e1e 100644 > --- a/test/tarantool-debugger-tests/debug-extension-tests.py > +++ b/test/tarantool-debugger-tests/debug-extension-tests.py > @@ -43,6 +43,8 @@ else: > # Don't run any initialization scripts. > RUN_CMD_FILE = ['--batch', '--nx', '--quiet', '--command'] > > +TEST_VERBOSE = os.getenv('DUBUGGER_TEST_VERBOSE', default=False) > Typo: /DU/DE/ > > + > RX_ADDR = r'0x[a-f0-9]+' > RX_HASH = RX_ADDR # The same pattern for hexademic values. > RX_BCN = r'00\d\d' > @@ -52,7 +54,7 @@ RX_IRREF = r'0x\d\d\d\d' > > > def persist(data): > - tmp = tempfile.NamedTemporaryFile(mode='w') > + tmp = tempfile.NamedTemporaryFile(mode='w', delete=not TEST_VERBOSE) > tmp.write(data) > tmp.flush() > return tmp > @@ -149,7 +151,12 @@ class TestCaseBase(unittest.TestCase): > LUAJIT_BINARY, > script_file.name, > ] > + if TEST_VERBOSE: > + print('# Test name: {}'.format(cls.__name__)) > + print('# Test command: {}'.format(' '.join(process_cmd))) > cls.output = execute_process(process_cmd) > + if TEST_VERBOSE: > + print('# Command output: {}'.format(cls.output)) > Please, append a new line before command (multiline) output. > > cmd_file.close() > script_file.close() > > -- > 2.54.0 > [-- Attachment #2: Type: text/html, Size: 3577 bytes --] ^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-06-28 1:31 UTC | newest] Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 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 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
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox