Tarantool development patches archive
 help / color / mirror / Atom feed
* [Tarantool-patches] [PATCH luajit 0/4] dbg: avoid hardcoded enums
@ 2026-11-12 23:42 Mikhail Elhimov via Tarantool-patches
  2026-11-12 23:42 ` [Tarantool-patches] [PATCH luajit 1/4] dbg: clean up and unification Mikhail Elhimov via Tarantool-patches
                   ` (3 more replies)
  0 siblings, 4 replies; 5+ messages in thread
From: Mikhail Elhimov via Tarantool-patches @ 2026-11-12 23:42 UTC (permalink / raw)
  To: Sergey Kaplun, Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches

Branch: https://github.com/tarantool/luajit/tree/elhimov/gh-13094-dbg-avoid-hardcoded-enums
Related issues:
  https://github.com/tarantool/tarantool/issues/13094
  https://github.com/tarantool/tarantool/issues/13159
  https://github.com/tarantool/tarantool/issues/4808

Mikhail Elhimov (4):
  dbg: clean up and unification
  dbg: avoid hardcoded enums
  dbg: display fast function name along with ffid
  dbg: remove wrong GC state

 src/luajit_dbg.py                             | 876 ++++++------------
 .../debug-extension-tests.py                  | 478 ++++++++--
 2 files changed, 695 insertions(+), 659 deletions(-)

-- 
2.55.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [Tarantool-patches] [PATCH luajit 1/4] dbg: clean up and unification
  2026-11-12 23:42 [Tarantool-patches] [PATCH luajit 0/4] dbg: avoid hardcoded enums Mikhail Elhimov via Tarantool-patches
@ 2026-11-12 23:42 ` Mikhail Elhimov via Tarantool-patches
  2026-11-12 23:42 ` [Tarantool-patches] [PATCH luajit 2/4] dbg: avoid hardcoded enums Mikhail Elhimov via Tarantool-patches
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 5+ messages in thread
From: Mikhail Elhimov via Tarantool-patches @ 2026-11-12 23:42 UTC (permalink / raw)
  To: Sergey Kaplun, Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches

* unified `dbg.eval` approach to simplify error handling (exception in
  case of any failure)
* removed trailing newline in all the dumpers to unify composing of the
  complex output
* newly introduced `dbg.print` is used whenever trailing newline is
  needed to make code more compact and clean
* moved handling of a missing command arg to command level (all the
  dump-like functions now expect concrete value)
* unified handling of command-level error (introduced special exception
  for LLDB that works similar to `gdb.GdbError`, i.e. doesn't cause
  traceback to be printed)
* in `gdb.eval()` dropped check of the value returned by
  `gdb.parse_and_eval()` as it would also fail for an expression like
 `'0'` (looks like a kind of experimental code that was left by mistake)
* `lldb.eval()` is adjusted to return object of the same type as
  `lldb.cast()` method returns (this improves consistency)
* renamed `dbg.eval` argument to reflect its meaning (it is
  an expression rather than a command)
---
 src/luajit_dbg.py                             | 178 +++++++++--------
 .../debug-extension-tests.py                  | 182 +++++++++++++-----
 2 files changed, 234 insertions(+), 126 deletions(-)

diff --git a/src/luajit_dbg.py b/src/luajit_dbg.py
index 80057a4e..77abf8c6 100644
--- a/src/luajit_dbg.py
+++ b/src/luajit_dbg.py
@@ -110,6 +110,10 @@ class Debugger(object):
                 self.write('{} command initialized\n'.format(name))
             self.write('LuaJIT debug extension is successfully loaded\n')
 
+    def print(self, msg):
+        '''Print the message with the new line.'''
+        self.write(msg + '\n')
+
     @abc.abstractmethod
     def cast(self, typestr, val):
         '''Cast the value to the required C type.'''
@@ -146,8 +150,8 @@ class Debugger(object):
         pass
 
     @abc.abstractmethod
-    def eval(self, command):
-        '''Parse and evaluate the given debugger command.'''
+    def eval(self, expr):
+        '''Parse and evaluate the given debugger expression.'''
         pass
 
     @abc.abstractmethod
@@ -268,14 +272,8 @@ class _GDBDebugger(Debugger):
         else:
             return None
 
-    def eval(self, command):
-        if not command:
-            return None
-
-        ret = gdb.parse_and_eval(command)
-        if not ret:
-            raise gdb.GdbError('table argument empty')
-        return ret
+    def eval(self, expr):
+        return gdb.parse_and_eval(expr)
 
     def detect_arch(self):
         if hasattr(self, 'arch'):
@@ -670,15 +668,26 @@ class _LLDBDebugger(Debugger):
         else:
             return None
 
-    def eval(self, command):
-        if not command:
-            return None
-
+    def eval(self, expr):
         process = self.target.GetProcess()
         thread = process.GetSelectedThread()
         frame = thread.GetSelectedFrame()
-        ret = frame.EvaluateExpression(command)
-        return ret
+        val = frame.EvaluateExpression(expr)
+        # LLDB doesn't throw an exception if it fails to
+        # evaluate an expression. In order to simplify handling
+        # of `Debugger.eval()` it is convenient to make it work
+        # similar to GDB, i.e. throw an exception in case of
+        # failure.
+        # Be aware that SBValue.IsValid() is necessary, but not
+        # sufficient as it only indicates that the value object
+        # does contain the relevant data (including the potential
+        # error), so it is necessary to check the `error` property
+        # as well.
+        if not val.IsValid():
+            raise self.error('{}: invalid expression'.format(expr))
+        if val.error.fail:
+            raise self.error(val.error)
+        return lldb.value(val)
 
     def detect_arch(self):
         if hasattr(self, 'arch'):
@@ -694,8 +703,13 @@ class _LLDBDebugger(Debugger):
     def write(self, msg):
         sys.stdout.write(msg)
 
+    class DbgError(Exception):
+        '''Raised to indicate an error during command execution.
+        Doesn't cause a traceback to be printed (like gdb.GdbError for GDB).'''
+        pass
+
     def error(self, errmsg):
-        return Exception(errmsg)
+        return self.DbgError(errmsg)
 
     def check_libluajit(self):
         # TODO: Implement postpone loading for LLDB too.
@@ -738,12 +752,27 @@ class _LLDBDebugger(Debugger):
         def __call__(ljbase, debugger, args, exe_ctx, result):
             try:
                 ljbase.execute(args)
-            except Exception as e:
+            except _LLDBDebugger.DbgError as e:
                 msg = 'Failed to execute command `{}`: {}'.format(
                     ljbase.name,
                     e
                 )
                 result.SetError(msg)
+                # `result` is an output argument of LLDB command
+                # handler. If error message is set, LLDB displays
+                # it in session. But such a message is missing in
+                # output when the same problem occurs in batch
+                # mode, so in order to be able to catch this kind
+                # of error in batch mode it is needed to put it
+                # into stderr explicitly.
+                # Also to avoid duplication of the same error
+                # message in interactive mode, it should be done
+                # in batch mode only. There is no official way to
+                # check if LLDB is in the batch mode so one of the
+                # recommended workarounds is used below that
+                # checks if tty is not used for standard input.
+                if not sys.stdin.isatty():
+                    result.PutError(lldb.SBFile.Create(sys.stderr, True))
 
         @abc.abstractmethod
         def execute(ljbase, args):
@@ -2399,7 +2428,7 @@ def dump_proto(proto):
     startbc = proto_bc(proto)
     func_loc = proto_loc(proto)
     # Location has the following format: '{chunk}:{firstline}'.
-    dump = '{func_loc}-{lastline}\n'.format(
+    dump = '{func_loc}-{lastline}'.format(
         func_loc=func_loc,
         lastline=proto['firstline'] + proto['numline'],
     )
@@ -2408,9 +2437,9 @@ def dump_proto(proto):
         return '=> ' + str(npc_from + delta).zfill(4)
 
     for bcnum in range(0, int(proto['sizebc'])):
-        dump += (str(bcnum).zfill(4) + ' ' + dump_bc(
+        dump += ('\n' + str(bcnum).zfill(4) + ' ' + dump_bc(
             startbc[bcnum], jmp_format=jmp_format, jmp_ctx=bcnum, proto=proto,
-        ) + '\n')
+        ))
     return dump
 
 
@@ -2421,9 +2450,9 @@ def dump_func(func):
         pt = funcproto(func)
         return dump_proto(pt)
     elif ffid == 1:
-        return 'C function @ {}\n'.format(strx64(func['f']))
+        return 'C function @ {}'.format(strx64(func['f']))
     else:
-        return 'fast function #{}\n'.format(int(ffid))
+        return 'fast function #{}'.format(int(ffid))
 
 
 # FFI dumpers.
@@ -2740,7 +2769,7 @@ def dump_irins(irins, trace=None):
         if op1 != -1:
             args = dump_call_args(trace, int(op1))
 
-        return '{flags} {type} {name:6} [{mode:2}] {f}({args}) {ct}\n'.format(
+        return '{flags} {type} {name:6} [{mode:2}] {f}({args}) {ct}'.format(
             flags=flags,
             name=irname,
             mode=ir_mode(irop),
@@ -2792,7 +2821,7 @@ def dump_irins(irins, trace=None):
     else:
         typename = IRTYPES[irt_type(irt)]
 
-    return '{flags} {type} {name:6} [{mode:2}] {left:<9s} {right}\n'.format(
+    return '{flags} {type} {name:6} [{mode:2}] {left:<9s} {right}'.format(
         flags=flags,
         name=irname,
         mode=ir_mode(irop),
@@ -2857,18 +2886,19 @@ def dump_regsp(irins, ins_number):
 
 
 def dump_trace(trace, flags):
-    dump = 'Trace {num} start\n\tproto: {start_pt}\n\tBC: {start_bc}\n'.format(
+    dump = 'Trace {num} start\n\tproto: {start_pt}\n\tBC: {start_bc}'.format(
         num=trace['traceno'],
         start_pt=gcref(trace['startpt']),
         start_bc=mref('BCIns *', trace['startpc']),
     )
 
     nins = trace['nins'] - REF_BIAS
-    dump += '---- TRACE IR\n'
+    dump += '\n---- TRACE IR'
     nsnap = 0
     snap = trace['snap'][nsnap]
     snapref = snap['ref']
     for irnum in range(1, nins):
+        dump += '\n'
         irref = REF_BIAS + irnum
         if 's' in flags and irref >= snapref and nsnap < trace['nsnap']:
             dump += '....          '
@@ -2894,27 +2924,32 @@ def dump_tref(tref):
     )
 
 
-def dump_jslots(coroutine):
-    lstate = L(None)
-    g = G(lstate or coroutine)
-    j = J(g)
-
-    dump = ''
-    maxslot = j['baseslot'] + j['maxslot']
+def dump_jslots(J):
+    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]
+        tref = J['slot'][n]
         ref = tref_ref(tref)
         address = dbg.address(tref)
-        dump += '{addr} {nslot:04d} {base:1s} {tref}{const}\n'.format(
+        dump.append('{addr} {nslot:04d} {base:1s} {tref}{const}'.format(
             addr=address,
-            base='B' if address == j['base'] else ' ',
+            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
+            const=' ' + dump_irk(J['cur'], ref - REF_BIAS)
+                  if ref != 0 and ref < REF_BIAS else ''
+        ))
+    return '\n'.join(dump)
+
+
+# Extension commands helpers. ####################################
+
+
+def cast_command_arg(typestr, arg):
+    if not arg:
+        raise dbg.error('Argument is required')
+    return dbg.cast(typestr, dbg.eval(arg))
 
 
 # Extension commands. ############################################
@@ -2931,8 +2966,8 @@ compile-time flag to inspect if LuaJIT is built in dual-number mode.
     '''
 
     def execute(self, arg):
-        dbg.write(
-            'LJ_64: {LJ_64}, LJ_GC64: {LJ_GC64}, LJ_DUALNUM: {LJ_DUALNUM}\n'
+        dbg.print(
+            'LJ_64: {LJ_64}, LJ_GC64: {LJ_GC64}, LJ_DUALNUM: {LJ_DUALNUM}'
             .format(
                 LJ_64=LJ_64,
                 LJ_GC64=LJ_GC64,
@@ -2964,9 +2999,7 @@ https://github.com/tarantool/tarantool/wiki/LuaJIT-Bytecodes.
     '''
 
     def execute(self, arg):
-        dbg.write('{}\n'.format(
-            dump_bc(dbg.cast('BCIns *', dbg.eval(arg))[0])
-        ))
+        dbg.print(dump_bc(cast_command_arg('BCIns *', arg)[0]))
 
 
 class LJDumpCType(dbg.LJBase):
@@ -2978,9 +3011,7 @@ 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)))
-        ))
+        dbg.print(dump_ctype(cast_command_arg('CType *', arg)))
 
 
 class LJDumpFunc(dbg.LJBase):
@@ -3005,7 +3036,7 @@ function:
     '''
 
     def execute(self, arg):
-        dbg.write('{}'.format(dump_func(dbg.cast('GCfuncC *', dbg.eval(arg)))))
+        dbg.print(dump_func(cast_command_arg('GCfuncC *', arg)))
 
 
 class LJGC(dbg.LJBase):
@@ -3029,7 +3060,7 @@ The command requires no args and dumps current GC stats:
 
     def execute(self, arg):
         g = G(L(None))
-        dbg.write('GC stats: {state}\n{stats}\n'.format(
+        dbg.print('GC stats: {state}\n{stats}'.format(
             state=gc_state(g),
             stats=dump_gc(g)
         ))
@@ -3060,8 +3091,7 @@ error message occurs.
     '''
 
     def execute(self, arg):
-        gcobj = dbg.cast('GCobj *', dbg.eval(arg))
-        dbg.write('{}\n'.format(dump_gcobj(gcobj)))
+        dbg.print(dump_gcobj(cast_command_arg('GCobj *', arg)))
 
 
 class LJDumpIR(dbg.LJBase):
@@ -3078,7 +3108,7 @@ https://github.com/tarantool/tarantool/wiki/LuaJIT-SSA-IR.
     '''
 
     def execute(self, arg):
-        dbg.write('{}'.format(dump_irins(dbg.cast('IRIns *', dbg.eval(arg)))))
+        dbg.print(dump_irins(cast_command_arg('IRIns *', arg)))
 
 
 class LJDumpJSlots(dbg.LJBase):
@@ -3095,9 +3125,8 @@ 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)))
-        ))
+        lstate = L(dbg.eval(arg) if arg else None)
+        dbg.print(dump_jslots(J(G(lstate))))
 
 
 class LJDumpProto(dbg.LJBase):
@@ -3122,9 +3151,7 @@ function:
     '''
 
     def execute(self, arg):
-        dbg.write('{}'.format(
-            dump_proto(dbg.cast('GCproto *', dbg.eval(arg)))
-        ))
+        dbg.print(dump_proto(cast_command_arg('GCproto *', arg)))
 
 
 class LJDumpStack(dbg.LJBase):
@@ -3161,7 +3188,8 @@ If L is omitted, the main coroutine is used.
     '''
 
     def execute(self, arg):
-        dbg.write('{}\n'.format(dump_stack(L(dbg.eval(arg)))))
+        lstate = L(dbg.eval(arg) if arg else None)
+        dbg.print(dump_stack(lstate))
 
 
 class LJState(dbg.LJBase):
@@ -3175,13 +3203,13 @@ The command requires no args and dumps current VM and GC states:
 
     def execute(self, arg):
         g = G(L(None))
-        dbg.write('{}\n'.format('\n'.join(
+        dbg.print('\n'.join(
             map(lambda t: '{} state: {}'.format(*t), {
                 'VM':  vm_state(g),
                 'GC':  gc_state(g),
                 'JIT': jit_state(g),
             }.items())
-        )))
+        ))
 
 
 class LJDumpString(dbg.LJBase):
@@ -3196,8 +3224,8 @@ is replaced with the corresponding error when decoding fails.
     '''
 
     def execute(self, arg):
-        string = dbg.cast('GCstr *', dbg.eval(arg))
-        dbg.write("String: {body} [{len} bytes] with hash {hash}\n".format(
+        string = cast_command_arg('GCstr *', arg)
+        dbg.print("String: {body} [{len} bytes] with hash {hash}".format(
             body=strdata(string),
             hash=strx64(string['hash']),
             len=string['len'],
@@ -3217,7 +3245,7 @@ The command receives a GCtab address and dumps the table contents:
     '''
 
     def execute(self, arg):
-        t = dbg.cast('GCtab *', dbg.eval(arg))
+        t = cast_command_arg('GCtab *', arg)
         array = mref('TValue *', t['array'])
         nodes = mref('struct Node *', t['node'])
         mt = gcval(t['metatable'])
@@ -3227,22 +3255,22 @@ The command receives a GCtab address and dumps the table contents:
         }
 
         if mt != 0:
-            dbg.write('Metatable detected: {}\n'.format(strx64(mt)))
+            dbg.print('Metatable detected: {}'.format(strx64(mt)))
 
-        dbg.write('Array part: {} slots\n'.format(capacity['apart']))
+        dbg.print('Array part: {} slots'.format(capacity['apart']))
         for i in range(capacity['apart']):
             slot = array + i
-            dbg.write('{ptr}: [{index}]: {value}\n'.format(
+            dbg.print('{ptr}: [{index}]: {value}'.format(
                 ptr=slot,
                 index=i,
                 value=dump_tvalue(slot)
             ))
 
-        dbg.write('Hash part: {} nodes\n'.format(capacity['hpart']))
+        dbg.print('Hash part: {} nodes'.format(capacity['hpart']))
         # See hmask comment in lj_obj.h
         for i in range(capacity['hpart']):
             node = nodes + i
-            dbg.write('{ptr}: {{ {key} }} => {{ {val} }}; next = {n}\n'.format(
+            dbg.print('{ptr}: {{ {key} }} => {{ {val} }}; next = {n}'.format(
                 ptr=node,
                 key=dump_tvalue(node['key']),
                 val=dump_tvalue(node['val']),
@@ -3280,8 +3308,7 @@ error message occurs.
     '''
 
     def execute(self, arg):
-        tv = dbg.cast('TValue *', dbg.eval(arg))
-        dbg.write('{}\n'.format(dump_tvalue(tv)))
+        dbg.print(dump_tvalue(cast_command_arg('TValue *', arg)))
 
 
 class LJDumpTrace(dbg.LJBase):
@@ -3300,10 +3327,7 @@ Trace may be preceded with /FLAGS:
 
     def execute(self, arg):
         arg, flags = dbg.extract_flags(arg, 'rs')
-        dbg.write('{}'.format(dump_trace(
-            dbg.cast('GCtrace *', dbg.eval(arg)),
-            flags
-        )))
+        dbg.print(dump_trace(cast_command_arg('GCtrace *', arg), flags))
 
 
 def load(event=None):
diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py
index ab44121c..fe8c5028 100644
--- a/test/tarantool-debugger-tests/debug-extension-tests.py
+++ b/test/tarantool-debugger-tests/debug-extension-tests.py
@@ -19,30 +19,6 @@ LLDB = 'lldb' in DEBUGGER
 EXTENSION = EXTENSION_PATH + '/luajit_dbg.py'
 TIMEOUT = 10
 
-if LLDB:
-    INFERIOR_ARGS = '--'
-    LOAD_EXTENSION = 'command script import ' + EXTENSION
-    PROCESS_RUN = (
-        # Prevent errors in case when running tests in Docker.
-        'settings set target.disable-aslr false\n'
-        'process launch'
-    )
-    # Don't run any initialization scripts.
-    RUN_CMD_FILE = [
-        '--batch',
-        '--no-lldbinit',
-        '--no-use-colors',
-        '--source-quietly',
-        '--source'
-    ]
-else:
-    # GDB.
-    INFERIOR_ARGS = '--args'
-    LOAD_EXTENSION = 'source ' + EXTENSION
-    PROCESS_RUN = 'run'
-    # Don't run any initialization scripts.
-    RUN_CMD_FILE = ['--batch', '--nx', '--quiet', '--command']
-
 TEST_VERBOSE = os.getenv('DEBUGGER_TEST_VERBOSE', default=False)
 
 RX_ADDR = r'0x[a-f0-9]+'
@@ -60,7 +36,7 @@ def persist(data):
     return tmp
 
 
-def execute_process(cmd, timeout=TIMEOUT):
+def execute_process_raw(cmd, timeout=TIMEOUT):
     if LEGACY:
         # XXX: The Python 2.7 version of `subprocess.Popen`
         # doesn't have a timeout option, so the required
@@ -68,12 +44,12 @@ def execute_process(cmd, timeout=TIMEOUT):
         process = subprocess.Popen(
             cmd,
             stdout=subprocess.PIPE,
-            stderr=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
             # This prevents sending of SIGSTTOU to the test when
             # running by `make'. Stdin is unused anyway.
             stdin=subprocess.DEVNULL
         )
-        timer = Timer(TIMEOUT, process.kill)
+        timer = Timer(timeout, process.kill)
         timer.start()
         stdout, _ = process.communicate()
         timer.cancel()
@@ -87,16 +63,71 @@ def execute_process(cmd, timeout=TIMEOUT):
         process = subprocess.run(
             cmd,
             stdout=subprocess.PIPE,
-            stderr=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
             # This prevents sending of SIGSTTOU to the test when
             # running by `make'. Stdin is unused anyway.
             stdin=subprocess.DEVNULL,
             universal_newlines=True,
-            timeout=TIMEOUT
+            timeout=timeout,
         )
         return process.stdout
 
 
+if LLDB:
+    INFERIOR_ARGS = '--'
+    LOAD_EXTENSION = 'command script import ' + EXTENSION
+    PROCESS_RUN = (
+        # Prevent errors in case when running tests in Docker.
+        'settings set target.disable-aslr false\n'
+        'process launch'
+    )
+    # Don't run any initialization scripts.
+    RUN_CMD_FILE = [
+        '--batch',
+        '--no-lldbinit',
+        '--no-use-colors',
+        '--source-quietly',
+        '--source'
+    ]
+    SET_EXEC = 'target create ' + LUAJIT_BINARY
+    SET_RUN_ARGS = 'settings set target.run-args'
+
+    def execute_process(cmd, timeout=TIMEOUT):
+        # While executing extension commands LLDB might produce
+        # errors that cannot be caught in extension. The internal
+        # parser does some processing in background and might
+        # emit an error message into stderr asynchronously. Also
+        # the internal parser has a kind of fallback mechanism
+        # that might resolve the problem, but is not able to
+        # "cancel" the error message which might fail tests in
+        # this case.
+        #
+        # List of regex that specifies lines to be wiped out from
+        # the LLDB output. In case additional cleaning is needed,
+        # just append this list with the correponding regex (use
+        # as strict regex as possible in order to avoid accidental
+        # removing of relevant data).
+        RX_LINES_TO_REMOVE = [
+            # This error occurs while parsing `union FPRCBArg` (it
+            # looks like either offset of `f`` field or size of
+            # `union FPRCBArg` is identified incorrectly).
+            r".*DW_TAG_member 'f' refers to type \w+ which extends beyond.*",
+        ]
+        raw_out = execute_process_raw(cmd, timeout)
+        return re.sub(r'^({})\n'.format('|'.join(RX_LINES_TO_REMOVE)),
+                      '', raw_out, flags=re.M)
+
+else:
+    # GDB.
+    INFERIOR_ARGS = '--args'
+    LOAD_EXTENSION = 'source ' + EXTENSION
+    PROCESS_RUN = 'run'
+    # Don't run any initialization scripts.
+    RUN_CMD_FILE = ['--batch', '--nx', '--quiet', '--command']
+    SET_EXEC = 'file ' + LUAJIT_BINARY
+    SET_RUN_ARGS = 'set args'
+    execute_process = execute_process_raw
+
 IS_DUALNUM = execute_process([
     LUAJIT_BINARY, '-e', "print(require('ffi').abi('dualnum'))"
 ]).strip() == 'true'
@@ -129,42 +160,38 @@ else:
 
 class TestCaseBase(unittest.TestCase):
     @classmethod
-    def construct_cmds(cls):
-        return '\n'.join([
+    def construct_cmds(cls, deferred, *run_args):
+        cmds = [
+            SET_EXEC,
+            SET_RUN_ARGS + ' ' + ' '.join(run_args),
             'b {loc}'.format(loc=cls.location),
             PROCESS_RUN,
             'n',
-            LOAD_EXTENSION,
             cls.extension_cmds.strip(),
-            'q',
-        ])
+        ]
+        cmds.insert(0 if deferred else 1, LOAD_EXTENSION)
+        return '\n'.join(cmds)
 
-    @classmethod
-    def setUpClass(cls):
-        cmd_file = persist(cls.construct_cmds())
-        script_file = persist(cls.lua_script)
+    def check(self, deferred=False):
+        script_file = persist(self.lua_script)
+        cmd_file = persist(self.construct_cmds(deferred, script_file.name))
         process_cmd = [
             DEBUGGER,
             *RUN_CMD_FILE,
             cmd_file.name,
-            INFERIOR_ARGS,
-            LUAJIT_BINARY,
-            script_file.name,
         ]
         if TEST_VERBOSE:
-            print('# Test name: {}'.format(cls.__name__))
+            print('# Test name: {}'.format(self.__class__.__name__))
             print('# Test command: {}'.format(' '.join(process_cmd)))
-        cls.output = execute_process(process_cmd)
+        output = execute_process(process_cmd)
         if TEST_VERBOSE:
-            print('# Command output:\n{}'.format(cls.output))
+            print('# Command output:\n{}'.format(output))
         cmd_file.close()
         script_file.close()
-
-    def check(self):
         if LEGACY:
-            self.assertRegexpMatches(self.output, self.pattern.strip())
+            self.assertRegexpMatches(output, self.pattern.strip())
         else:
-            self.assertRegex(self.output, self.pattern.strip())
+            self.assertRegex(output, self.pattern.strip())
 
 
 # Test that the emitted debug information supports macro
@@ -1051,8 +1078,65 @@ class TestLJCTypeBase(TestCaseBase):
     pattern = r'\[\d+\] <int>'
 
 
-for test_cls in TestCaseBase.__subclasses__():
+class TestMissingArgBase(TestCaseBase):
+    location = 'lj_cf_print'
+    lua_script = 'print(1)'
+    pattern = r'Argument is required\n'
+
+    @classmethod
+    def setUpClass(cls):
+        cls.extension_cmds = '{}\n'.format(cls.cmd_name)
+        super(TestMissingArgBase, cls).setUpClass()
+
+
+class TestMissingArgLJFunc(TestMissingArgBase):
+    cmd_name = 'lj-func'
+
+
+class TestMissingArgLJTValue(TestMissingArgBase):
+    cmd_name = 'lj-tv'
+
+
+class TestMissingArgLJBC(TestMissingArgBase):
+    cmd_name = 'lj-bc'
+
+
+class TestMissingArgLJStr(TestMissingArgBase):
+    cmd_name = 'lj-str'
+
+
+class TestMissingArgLJTable(TestMissingArgBase):
+    cmd_name = 'lj-tab'
+
+
+class TestMissingArgLJProto(TestMissingArgBase):
+    cmd_name = 'lj-proto'
+
+
+class TestMissingArgLJGCO(TestMissingArgBase):
+    cmd_name = 'lj-gco'
+
+
+class TestMissingArgLJCType(TestMissingArgBase):
+    cmd_name = 'lj-ctype'
+
+
+def get_leaf_subclasses(cls):
+    subclasses = cls.__subclasses__()
+    if not subclasses:
+        yield cls
+    else:
+        for sub in subclasses:
+            for leaf in get_leaf_subclasses(sub):
+                yield leaf
+
+
+for test_cls in get_leaf_subclasses(TestCaseBase):
     test_cls.test = lambda self: self.check()
+    test_cls.test_with_deferred_loading = \
+        unittest.skipIf(LLDB, "not supported for LLDB")(
+            lambda self: self.check(True)
+        )
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)
-- 
2.55.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [Tarantool-patches] [PATCH luajit 2/4] dbg: avoid hardcoded enums
  2026-11-12 23:42 [Tarantool-patches] [PATCH luajit 0/4] dbg: avoid hardcoded enums Mikhail Elhimov via Tarantool-patches
  2026-11-12 23:42 ` [Tarantool-patches] [PATCH luajit 1/4] dbg: clean up and unification Mikhail Elhimov via Tarantool-patches
@ 2026-11-12 23:42 ` Mikhail Elhimov via Tarantool-patches
  2026-11-12 23:42 ` [Tarantool-patches] [PATCH luajit 3/4] dbg: display fast function name along with ffid Mikhail Elhimov via Tarantool-patches
  2026-11-12 23:42 ` [Tarantool-patches] [PATCH luajit 4/4] dbg: remove wrong GC state Mikhail Elhimov via Tarantool-patches
  3 siblings, 0 replies; 5+ messages in thread
From: Mikhail Elhimov via Tarantool-patches @ 2026-11-12 23:42 UTC (permalink / raw)
  To: Sergey Kaplun, Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches

Besides reducing lines of code this way the extension becomes compatible
with various versions of LuaJIT because different versions might use
different sets of enum members (a newer version might introduce
additional BC/IR/etc.)

Prior to this patch, the string was used as a debugger-agnostic way to
specify type, but this way might not work in the case of an enum because
debugging information might be optimized out if no variable of such an
enum type is declared and its members are used only as predefined
constants. The type of enum is needed to be able to get human-readable
value (member name instead of number). The alternative way to get enum
type is to get it from the value, i.e. somehow obtain the value that is
of enum type and then use this type to convert the numbers to the member
names.

Creating the value of the given enum member is quite different in GDB
and LLDB, so separate method `create_enum_value` was introduced in
Debugger API. Also new method `cast_typeof` was introduced to perform
value-based cast, i.e. it casts to the same type as a reference value.

Also this patch fixes the mapping of FPMATHOP to a human-readable form,
because prior to this patch, IRFPMS contained the incorrect entry
'exp2'. IRFPMS is a human-readable form of enum IRFPMathOp, but there is
no 'exp2' enum member actually (see IRFPMDEF). It looks like it was
added by mistake initially (corresponding tests to check mapping of
FPMATHOP were added).

Resolves tarantool/tarantool#13094
Resolves tarantool/tarantool#13159
---
 src/luajit_dbg.py                             | 675 +++++-------------
 .../debug-extension-tests.py                  |  62 +-
 2 files changed, 249 insertions(+), 488 deletions(-)

diff --git a/src/luajit_dbg.py b/src/luajit_dbg.py
index 77abf8c6..ceba4c58 100644
--- a/src/luajit_dbg.py
+++ b/src/luajit_dbg.py
@@ -106,6 +106,8 @@ class Debugger(object):
     def initialize_extension(self, commands):
         if self.configure():
             for name, command in commands.items():
+                if hasattr(command, 'pre_register'):
+                    command.pre_register()
                 self.register_command(command, name)
                 self.write('{} command initialized\n'.format(name))
             self.write('LuaJIT debug extension is successfully loaded\n')
@@ -119,6 +121,13 @@ class Debugger(object):
         '''Cast the value to the required C type.'''
         pass
 
+    @abc.abstractmethod
+    def cast_typeof(self, ref_val, val):
+        '''Cast the value to the type of the given object
+        It is used mostly for casting when the string name of the type isn't
+        accessible (for example, anonymous enums).'''
+        pass
+
     @abc.abstractmethod
     def sizeof(self, typestr):
         '''Return the size of the given type in bytes.'''
@@ -191,6 +200,16 @@ class Debugger(object):
         '''Register the command with the corresponding name.'''
         pass
 
+    # Creating the value of the given enum member is quite
+    # different in GDB and LLDB, so this method is needed to hide
+    # the implementation details.
+    @abc.abstractmethod
+    def create_enum_value(self, enum_member_name, enum_name=None):
+        '''Return debugger-specific value object that represents
+        the given enum member.
+        '''
+        pass
+
     @abc.abstractproperty
     def LJBase(self):
         '''Base command class.
@@ -218,6 +237,9 @@ class _GDBDebugger(Debugger):
     def cast(self, typestr, val):
         return gdb.Value(val).cast(self._dbgtype(typestr))
 
+    def cast_typeof(self, ref_val, val):
+        return gdb.Value(val).cast(ref_val.type)
+
     def sizeof(self, typestr):
         return self._dbgtype(typestr).sizeof
 
@@ -338,6 +360,15 @@ class _GDBDebugger(Debugger):
     def register_command(self, command, name):
         command(name)
 
+    def create_enum_value(self, enum_member_name, enum_name=None):
+        val = self.eval(enum_member_name)
+        if val.type.code != gdb.TYPE_CODE_ENUM:
+            raise self.error('{}: expected enum, got type {}'.format(
+                enum_member_name,
+                val.type.code,
+            ))
+        return val
+
     class LJBase(gdb and gdb.Command or object):
         def __init__(ljbase, name):
             # XXX Fragile: Though the command initialization looks
@@ -376,6 +407,10 @@ class _LLDBDebugger(Debugger):
             lldb.eBasicTypeInt128
         ]
 
+    def _lldb_tp_isenum(self, tp):
+        return tp.GetCanonicalType().GetTypeClass() == \
+            lldb.eTypeClassEnumeration
+
     def _lldb_value_from_raw(self, raw_value, size, tp):
         isfp = self._lldb_tp_isfp(tp)
         if isfp:
@@ -489,8 +524,7 @@ class _LLDBDebugger(Debugger):
             # Instead of default GetSummary.
             if not lldbval.sbvalue.TypeIsPointerType():
                 tp = lldbval.sbvalue.GetType()
-                is_float = self._lldb_tp_isfp(tp)
-                if is_float:
+                if self._lldb_tp_isfp(tp) or self._lldb_tp_isenum(tp):
                     return lldbval.sbvalue.GetValue()
                 else:
                     return str(int(lldbval))
@@ -566,11 +600,10 @@ class _LLDBDebugger(Debugger):
         self.dbgtype_cache[typestr] = dbgtype
         return dbgtype
 
-    def cast(self, typestr, val):
+    def _cast(self, tp, val):
         if isinstance(val, lldb.value):
             val = val.sbvalue
         elif type(val) is int:
-            tp = self._dbgtype(typestr)
             return self._lldb_value_from_raw(val, tp.GetByteSize(), tp)
         elif not isinstance(val, lldb.SBValue):
             raise Exception(
@@ -580,7 +613,6 @@ class _LLDBDebugger(Debugger):
         # XXX: Simply SBValue.Cast() works incorrectly since it
         # may take the 8 bytes of memory instead of 4, before the
         # cast. Construct the value on the fly.
-        tp = self._dbgtype(typestr)
         if self._lldb_tp_isfp(tp):
             rawval = float(val.GetValue())
         elif self._lldb_tp_issigned(tp):
@@ -589,6 +621,12 @@ class _LLDBDebugger(Debugger):
             rawval = val.GetValueAsUnsigned()
         return self._lldb_value_from_raw(rawval, val.GetByteSize(), tp)
 
+    def cast(self, typestr, val):
+        return self._cast(self._dbgtype(typestr), val)
+
+    def cast_typeof(self, ref_val, val):
+        return self._cast(ref_val.sbvalue.type, val)
+
     def sizeof(self, typestr):
         return self._dbgtype(typestr).GetByteSize()
 
@@ -738,6 +776,55 @@ class _LLDBDebugger(Debugger):
             )
         )
 
+    def create_enum_value(self, enum_member_name, enum_name=None):
+        if enum_name is not None:
+            # In LLDB an enum member has to be specified in the
+            # form: <enum_name>::<enum_member_name>
+            expr = enum_name + "::" + enum_member_name
+            try:
+                val = self.eval(expr)
+            except Exception:
+                val = None
+
+            if val is not None:
+                if not self._lldb_tp_isenum(val.sbvalue.GetType()):
+                    raise self.error('{}: expected enum, got type {}'.format(
+                        expr,
+                        val.sbvalue.GetType().GetTypeClass(),
+                    ))
+                return val
+
+        # LLDB needs the enum name in the expression above but
+        # 1. Enum might be anonymous (like LJ_VMST__MAX)
+        # 2. Debugging information about the enum name might be
+        #    optimized out if no variable of the given enum type
+        #    is declared and its members are only used as the
+        #    predefined constants (like in case of the enum
+        #    IRFieldID)
+
+        # In these cases the above method doesn't work so trying
+        # to discover the enum type by the given enum member.
+
+        def find_enum_type_member(enum_type, enum_member_name):
+            # SBTypeEnumMemberList supports members iteration and
+            # [] access (both by index and by member name) only
+            # starting from lldb-12 so this implementation is used
+            # to handle earlier versions.
+            members = enum_type.GetEnumMembers()
+            for i in range(members.GetSize()):
+                item = members.GetTypeEnumMemberAtIndex(i)
+                if item.name == enum_member_name:
+                    return item
+            return None
+
+        for m in self.target.modules:
+            for et in m.GetTypes(lldb.eTypeClassEnumeration):
+                et_member = find_enum_type_member(et, enum_member_name)
+                if et_member is not None:
+                    return self._cast(et, et_member.unsigned)
+
+        raise self.error('{}: not found'.format(enum_member_name))
+
     class LJBase(object):
         # Ignore given parameters by LLDB.
         def __init__(ljbase, debugger, unused):
@@ -829,6 +916,55 @@ def strx64(val):
     return re.sub('L?$', '', hex(int(tou64(val))))
 
 
+class EnumBasedList(object):
+    def __init__(self, max_enum_member, enum_name, map_func=None,
+                 *map_func_args):
+        self.__max_enum_member = max_enum_member
+        self.__enum_name = enum_name
+        self.__map_func = map_func
+        self.__map_func_args = map_func_args
+        # Lazy initialization (see __get_items) as the required
+        # information might be unavailable at this moment.
+        self.__items = None
+
+    def __iter__(self):
+        return iter(self.__get_items())
+
+    def __getitem__(self, key):
+        return self.__get_items()[key]
+
+    def __len__(self):
+        return len(self.__get_items())
+
+    def __get_items(self):
+        if self.__items is None:
+            try:
+                max_enum_value = dbg.create_enum_value(
+                    self.__max_enum_member, self.__enum_name
+                )
+            except Exception as e:
+                msg = 'failed to resolve {} enum members using {}'.format(
+                    self.__enum_name or 'anonymous',
+                    self.__max_enum_member,
+                )
+                raise dbg.error('{}: {}'.format(msg, e))
+            items = []
+            for i in range(max_enum_value):
+                item = str(dbg.cast_typeof(max_enum_value, i))
+                if self.__map_func:
+                    item = self.__map_func(item, *self.__map_func_args)
+                items.append(item)
+            self.__items = items
+        return self.__items
+
+    def index(self, item):
+        return self.__get_items().index(item)
+
+
+def cut_prefix(s, prefix):
+    return s[len(prefix):] if s.startswith(prefix) else s
+
+
 # Types and TValues.
 
 
@@ -906,10 +1042,7 @@ def bc_d(ins):
     return int(ins) >> 16
 
 
-BCMODE = [
-    'none', 'dst', 'base', 'var', 'rbase', 'uv',
-    'lit', 'lits', 'pri', 'num', 'str', 'tab', 'func', 'jump', 'cdata',
-]
+BCMODE = EnumBasedList('BCM_max', 'BCMode', cut_prefix, 'BCM')
 
 
 lj_bc_mode_ = None
@@ -935,136 +1068,7 @@ def bcmode_cd(op):
     return int((lj_bc_mode()[op] >> 7) & 15)
 
 
-# Unfortunately, there is no place in the VM except the generated
-# Lua table, where the bytecode names are stored. So duplicate
-# them here.
-BYTECODES = [
-    # Comparison ops. ORDER OPR.
-    'ISLT',
-    'ISGE',
-    'ISLE',
-    'ISGT',
-
-    'ISEQV',
-    'ISNEV',
-    'ISEQS',
-    'ISNES',
-    'ISEQN',
-    'ISNEN',
-    'ISEQP',
-    'ISNEP',
-
-    # Unary test and copy ops.
-    'ISTC',
-    'ISFC',
-    'IST',
-    'ISF',
-    'ISTYPE',
-    'ISNUM',
-    'MOV',
-    'NOT',
-    'UNM',
-    'LEN',
-    'ADDVN',
-    'SUBVN',
-    'MULVN',
-    'DIVVN',
-    'MODVN',
-
-    # Binary ops. ORDER OPR.
-    'ADDNV',
-    'SUBNV',
-    'MULNV',
-    'DIVNV',
-    'MODNV',
-
-    'ADDVV',
-    'SUBVV',
-    'MULVV',
-    'DIVVV',
-    'MODVV',
-
-    'POW',
-    'CAT',
-
-    # Constant ops.
-    'KSTR',
-    'KCDATA',
-    'KSHORT',
-    'KNUM',
-    'KPRI',
-    'KNIL',
-
-    # Upvalue and function ops.
-    'UGET',
-    'USETV',
-    'USETS',
-    'USETN',
-    'USETP',
-    'UCLO',
-    'FNEW',
-
-    # Table ops.
-    'TNEW',
-    'TDUP',
-    'GGET',
-    'GSET',
-    'TGETV',
-    'TGETS',
-    'TGETB',
-    'TGETR',
-    'TSETV',
-    'TSETS',
-    'TSETB',
-    'TSETM',
-    'TSETR',
-
-    # Calls and vararg handling. T = tail call.
-    'CALLM',
-    'CALL',
-    'CALLMT',
-    'CALLT',
-    'ITERC',
-    'ITERN',
-    'VARG',
-    'ISNEXT',
-
-    # Returns.
-    'RETM',
-    'RET',
-    'RET0',
-    'RET1',
-
-    # Loops and branches. I/J = interp/JIT.
-    # I/C/L = init/call/loop.
-    'FORI',
-    'JFORI',
-
-    'FORL',
-    'IFORL',
-    'JFORL',
-
-    'ITERL',
-    'IITERL',
-    'JITERL',
-
-    'LOOP',
-    'ILOOP',
-    'JLOOP',
-
-    'JMP',
-
-    # Function headers. I/J = interp/JIT.
-    # F/V/C = fixarg/vararg/C func.
-    'FUNCF',
-    'IFUNCF',
-    'JFUNCF',
-    'FUNCV',
-    'IFUNCV',
-    'JFUNCV',
-    'FUNCC',
-    'FUNCCW',
-]
+BYTECODES = EnumBasedList('BC__MAX', 'BCOp', cut_prefix, 'BC_')
 
 
 def proto_bc(proto):
@@ -1219,42 +1223,16 @@ def J(g):
 
 
 # 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
+MM_NAMES = EnumBasedList('MM__MAX', 'MMS', cut_prefix, 'MM_')
 
 
 # Get the name of the index in the predefined arrays.
 def idx_name(field_name):
+    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
+
     # Don't use **{ to be compatible with Python 2.
     gcroot = {}
     gcroot.update({
@@ -1298,18 +1276,17 @@ def ggfname_by_offset(offset):
     return ggfname
 
 
+VM_STATES = EnumBasedList('LJ_VMST__MAX', None, cut_prefix, 'LJ_VMST_')
+
+
 def vm_state(g):
-    return {
-        i2notu32(0): 'INTERP',
-        i2notu32(1): 'LFUNC',
-        i2notu32(2): 'FFUNC',
-        i2notu32(3): 'CFUNC',
-        i2notu32(4): 'GC',
-        i2notu32(5): 'EXIT',
-        i2notu32(6): 'RECORD',
-        i2notu32(7): 'OPT',
-        i2notu32(8): 'ASM',
-    }.get(int(tou32(g['vmstate'])), 'TRACE')
+    index = i2notu32(g['vmstate'])
+    return VM_STATES[index] if index < len(VM_STATES) \
+        else vm_state_trace(~index)
+
+
+def vm_state_trace(traceno):
+    return 'TRACE(#{})'.format(traceno)
 
 
 def gc_state(g):
@@ -1506,140 +1483,7 @@ def cdataptr(cd):
 # 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',
-]
+IRS = EnumBasedList('IR__MAX', 'IROp', cut_prefix, 'IR_')
 
 
 # Mode bits: Commutative, {Normal/Ref, Alloc, Load, Store},
@@ -1691,71 +1535,31 @@ def ir_mode(op):
     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',
-]
+IRTYPES = EnumBasedList('IRT__MAX', 'IRType', lambda x: {
+                            'IRT_CDATA': 'cdt',
+                            'IRT_FLOAT': 'flt',
+                            'IRT_LIGHTUD': 'lud',
+                            'IRT_SOFTFP': 'sfp',
+                            'IRT_UDATA': 'udt',
+                        }.get(x, cut_prefix(x, 'IRT_')[:3].ljust(3).lower()))
 
 
-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',
-]
+IRT_NUM = None
 
 
-IRFPMS = [
-    'floor',
-    'ceil',
-    'trunc',
-    'sqrt',
-    'exp2',
-    'log',
-    'log2',
-    'other'
-]
+def irt_num():
+    global IRT_NUM
+    if IRT_NUM is None:
+        IRT_NUM = IRTYPES.index('num')
+    return IRT_NUM
+
+
+IRFIELDS = EnumBasedList('IRFL__MAX', 'IRFieldID', lambda x:
+                         cut_prefix(x, 'IRFL_').lower().replace('_', '.', 1))
+
+
+IRFPMS = EnumBasedList('IRFPM__MAX', 'IRFPMathOp',
+                       lambda x: cut_prefix(x, 'IRFPM_').lower())
 
 
 # Don't use *[ to be compatible with Python 2.
@@ -1784,112 +1588,7 @@ REGISTERS = {
 }
 
 
-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',
-]
+IR_CALLS = EnumBasedList('IRCALL__MAX', 'IRCallID', cut_prefix, 'IRCALL_')
 
 
 def regname(reg_number):
@@ -2026,7 +1725,7 @@ def irt_isguard(t):
 
 def irt_toitype(irt):
     t = irt_type(irt)
-    if LJ_DUALNUM and t > IRT_NUM:
+    if LJ_DUALNUM and t > irt_num():
         return LJ_T['NUMX']
     else:
         return i2notu32(t)
@@ -3193,13 +2892,15 @@ If L is omitted, the main coroutine is used.
 
 
 class LJState(dbg.LJBase):
-    '''
+    @classmethod
+    def pre_register(cls):
+        cls.__doc__ = '''
 lj-state
 The command requires no args and dumps current VM and GC states:
-* VM state: <INTERP|C|GC|EXIT|RECORD|OPT|ASM|TRACE>
+* VM state: <{}>
 * GC state: <PAUSE|PROPAGATE|ATOMIC|SWEEPSTRING|SWEEP|FINALIZE|LAST>
 * JIT state: <IDLE|ACTIVE|RECORD|START|END|ASM|ERR>
-    '''
+    '''.format('|'.join(list(VM_STATES) + [vm_state_trace('TRACENO')]))
 
     def execute(self, arg):
         g = G(L(None))
diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py
index fe8c5028..8a3ef96a 100644
--- a/test/tarantool-debugger-tests/debug-extension-tests.py
+++ b/test/tarantool-debugger-tests/debug-extension-tests.py
@@ -289,12 +289,26 @@ class TestLJArch(TestCaseBase):
     )
 
 
+RX_VM_STATE = r'({})'.format('|'.join([
+    r'INTERP',
+    r'LFUNC',
+    r'FFUNC',
+    r'CFUNC',
+    r'GC',
+    r'EXIT',
+    r'RECORD',
+    r'OPT',
+    r'ASM',
+    r'TRACE\(#\d+\)',
+]))
+
+
 class TestLJState(TestCaseBase):
     extension_cmds = 'lj-state'
     location = 'lj_cf_print'
     lua_script = 'print(1)'
     pattern = (
-        r'VM state: [A-Z]+\n'
+        r'VM state: ' + RX_VM_STATE + '\n'
         r'GC state: [A-Z]+\n'
         r'JIT state: [A-Z]+\n'
     )
@@ -857,6 +871,52 @@ class TestLJIRCallXSCType(TestCaseBase):
     )
 
 
+# Base class to check FPMATHOP mapping in FPMATH IR.
+class TestLJIRFPMathOpBase(TestCaseBase):
+    location = 'lj_cf_print'
+    extension_cmds = 'lj-trace &((GG_State *)L)->J->cur'
+
+    @classmethod
+    def setUpClass(cls):
+        cls.lua_script = (
+            'jit.opt.start("hotloop=1")\n'
+            'local function trace(a)\n'
+            '  local x = {}\n'
+            '  return x\n'
+            'end\n'
+            'trace(1.1)\n'
+            'trace(1.1)\n'
+            'print()\n'
+        ).format(cls.lua_expr)
+        cls.pattern = r'num FPMATH .* ref: ' + RX_IRN + r' lit: ' + cls.op
+        super(TestLJIRFPMathOpBase, cls).setUpClass()
+
+
+class TestLJIRFPMathFloor(TestLJIRFPMathOpBase):
+    lua_expr = 'math.floor(a)'
+    op = 'floor'
+
+
+class TestLJIRFPMathCeil(TestLJIRFPMathOpBase):
+    lua_expr = 'math.ceil(a)'
+    op = 'ceil'
+
+
+class TestLJIRFPMathSqrt(TestLJIRFPMathOpBase):
+    lua_expr = 'math.sqrt(a)'
+    op = 'sqrt'
+
+
+class TestLJIRFPMathLog(TestLJIRFPMathOpBase):
+    lua_expr = 'math.log(a)'
+    op = 'log'
+
+
+class TestLJIRFPMathLog2(TestLJIRFPMathOpBase):
+    lua_expr = 'math.log(a, 3)'
+    op = 'log2'
+
+
 class TestLJJSlotsBase(TestCaseBase):
     location = 'trace_stop'
     extension_cmds = (
-- 
2.55.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [Tarantool-patches] [PATCH luajit 3/4] dbg: display fast function name along with ffid
  2026-11-12 23:42 [Tarantool-patches] [PATCH luajit 0/4] dbg: avoid hardcoded enums Mikhail Elhimov via Tarantool-patches
  2026-11-12 23:42 ` [Tarantool-patches] [PATCH luajit 1/4] dbg: clean up and unification Mikhail Elhimov via Tarantool-patches
  2026-11-12 23:42 ` [Tarantool-patches] [PATCH luajit 2/4] dbg: avoid hardcoded enums Mikhail Elhimov via Tarantool-patches
@ 2026-11-12 23:42 ` Mikhail Elhimov via Tarantool-patches
  2026-11-12 23:42 ` [Tarantool-patches] [PATCH luajit 4/4] dbg: remove wrong GC state Mikhail Elhimov via Tarantool-patches
  3 siblings, 0 replies; 5+ messages in thread
From: Mikhail Elhimov via Tarantool-patches @ 2026-11-12 23:42 UTC (permalink / raw)
  To: Sergey Kaplun, Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches

Part of tarantool/tarantool#4808
---
 src/luajit_dbg.py                             |  22 +++-
 .../debug-extension-tests.py                  | 121 ++++++++++++++----
 2 files changed, 109 insertions(+), 34 deletions(-)

diff --git a/src/luajit_dbg.py b/src/luajit_dbg.py
index ceba4c58..db4b3f04 100644
--- a/src/luajit_dbg.py
+++ b/src/luajit_dbg.py
@@ -1178,6 +1178,11 @@ def frames(L):
 # LuaJIT macro implementations and structure access.
 
 
+# Get FastFunc enum members and replace any single '_' with '.'.
+FF_NAMES = EnumBasedList('FF__MAX', 'FastFunc', lambda x:
+                         re.sub('(?<!_)_', '.', cut_prefix(x, 'FF_')))
+
+
 def mref(typename, obj):
     return dbg.cast(typename, obj['ptr64'] if LJ_GC64 else obj['ptr32'])
 
@@ -1757,6 +1762,11 @@ def ir_kint64(ir):
 
 # Dumpers.
 
+def dump_fast_function(ffid):
+    ffname = FF_NAMES[ffid] if ffid < len(FF_NAMES) else 'unknown'
+    return 'fast function {} (#{})'.format(ffname, ffid)
+
+
 # GCobj dumpers.
 
 def dump_lj_gco_str(gcobj):
@@ -1780,7 +1790,7 @@ def dump_lj_gco_proto(gcobj):
 
 def dump_lj_gco_func(gcobj):
     func = dbg.cast('struct GCfuncC *', gcobj)
-    ffid = func['ffid']
+    ffid = int(func['ffid'])
 
     if ffid == 0:
         pt = funcproto(func)
@@ -1793,7 +1803,7 @@ def dump_lj_gco_func(gcobj):
     elif ffid == 1:
         return 'C function @ {}'.format(strx64(func['f']))
     else:
-        return 'fast function #{}'.format(int(ffid))
+        return dump_fast_function(ffid)
 
 
 def dump_lj_gco_trace(gcobj):
@@ -2143,7 +2153,7 @@ def dump_proto(proto):
 
 
 def dump_func(func):
-    ffid = func['ffid']
+    ffid = int(func['ffid'])
 
     if ffid == 0:
         pt = funcproto(func)
@@ -2151,7 +2161,7 @@ def dump_func(func):
     elif ffid == 1:
         return 'C function @ {}'.format(strx64(func['f']))
     else:
-        return 'fast function #{}'.format(int(ffid))
+        return dump_fast_function(ffid)
 
 
 # FFI dumpers.
@@ -2779,7 +2789,7 @@ the type and some info related to it.
 * LJ_TFUNC: <LFUNC|CFUNC|FFUNC>
   <LFUNC>: Lua function @ <gcr>, <nupvals> upvalues, <chunk:line>
   <CFUNC>: C function <mcode address>
-  <FFUNC>: fast function #<ffid>
+  <FFUNC>: fast function <ffname> (#<ffid>)
 * LJ_TTRACE: trace <traceno> @ <gcr>
 * LJ_TCDATA: cdata @ <gcr>
 * LJ_TTAB: table @ <gcr> (asize: <asize>, hmask: <hmask>)
@@ -2997,7 +3007,7 @@ the type and some info related to it.
 * LJ_TFUNC: <LFUNC|CFUNC|FFUNC>
   <LFUNC>: Lua function @ <gcr>, <nupvals> upvalues, <chunk:line>
   <CFUNC>: C function <mcode address>
-  <FFUNC>: fast function #<ffid>
+  <FFUNC>: fast function <ffname> (#<ffid>)
 * LJ_TTRACE: trace <traceno> @ <gcr>
 * LJ_TCDATA: cdata @ <gcr>
 * LJ_TTAB: table @ <gcr> (asize: <asize>, hmask: <hmask>)
diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py
index 8a3ef96a..9237d3f1 100644
--- a/test/tarantool-debugger-tests/debug-extension-tests.py
+++ b/test/tarantool-debugger-tests/debug-extension-tests.py
@@ -92,6 +92,9 @@ if LLDB:
     SET_EXEC = 'target create ' + LUAJIT_BINARY
     SET_RUN_ARGS = 'settings set target.run-args'
 
+    def modify_obj_cmd(obj, val):
+        return 'expression {} = {}'.format(obj, val)
+
     def execute_process(cmd, timeout=TIMEOUT):
         # While executing extension commands LLDB might produce
         # errors that cannot be caught in extension. The internal
@@ -128,6 +131,9 @@ else:
     SET_RUN_ARGS = 'set args'
     execute_process = execute_process_raw
 
+    def modify_obj_cmd(obj, val):
+        return 'set var {} = {}'.format(obj, val)
+
 IS_DUALNUM = execute_process([
     LUAJIT_BINARY, '-e', "print(require('ffi').abi('dualnum'))"
 ]).strip() == 'true'
@@ -361,13 +367,31 @@ class TestLJStackFunc(TestCaseBase):
     pattern = STACK_RX
 
 
+FFUNC_ARGS = (
+    'print,\n'
+    'math.min,\n'
+    'getmetatable(io.stdout).close,\n'
+    'getmetatable(io.stdout).__gc,\n'
+    'getmetatable(io.stdout).__tostring,\n'
+)
+
+
+FFUNC_RX = (
+    r'fast function print \(#[0-9]+\)\n'
+    r'fast function math.min \(#[0-9]+\)\n'
+    r'fast function io.method.close \(#[0-9]+\)\n'
+    r'fast function io.method.__gc \(#[0-9]+\)\n'
+    r'fast function io.method.__tostring \(#[0-9]+\)\n'
+)
+
+
 # Sorted in LJT order.
 GCO_ARGS = (
     '"hello",\n'
     'coroutine.create(function() end),\n'
     'function() end,\n'
-    'require,\n'
-    'print,\n'
+    'require,\n' +
+    FFUNC_ARGS +
     'ffi.new("int*"),\n'
     '{1},\n'
     'newproxy(),\n'
@@ -378,8 +402,8 @@ GCO_RX = (
     r'string \"hello\" @ ' + RX_ADDR + r'\n'
     r'thread @ ' + RX_ADDR + r'\n'
     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'C function @ ' + RX_ADDR + r'\n' +
+    FFUNC_RX +
     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'
@@ -388,21 +412,9 @@ GCO_RX = (
 
 class TestLJTV(TestCaseBase):
     location = 'lj_cf_print'
-    extension_cmds = (
-        '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'
+    extension_cmds = ''.join(
+        ('lj-tv L->base+{}\n'.format(i)
+         for i in range(4 + GCO_ARGS.count('\n') + 2))
     )
 
     # Sorted in LJT order.
@@ -463,15 +475,9 @@ class TestLJTab(TestCaseBase):
 
 class TestLJGCo(TestCaseBase):
     location = 'lj_cf_print'
-    extension_cmds = (
-        'lj-gco ' + gcval('L->base + 0') + '\n'
-        'lj-gco ' + gcval('L->base + 1') + '\n'
-        'lj-gco ' + gcval('L->base + 2') + '\n'
-        'lj-gco ' + gcval('L->base + 3') + '\n'
-        'lj-gco ' + gcval('L->base + 4') + '\n'
-        'lj-gco ' + gcval('L->base + 5') + '\n'
-        'lj-gco ' + gcval('L->base + 6') + '\n'
-        'lj-gco ' + gcval('L->base + 7') + '\n'
+    extension_cmds = ''.join(
+        ('lj-gco ' + gcval('L->base + {}'.format(i)) + '\n'
+         for i in range(GCO_ARGS.count('\n')))
     )
 
     lua_script = (
@@ -485,6 +491,65 @@ class TestLJGCo(TestCaseBase):
     pattern = GCO_RX
 
 
+class TestLJGCoFFUnknown(TestCaseBase):
+    location = 'lj_cf_print'
+
+    val = '((GCfuncC *)' + gcval('L->base') + ')'
+    extension_cmds = (
+        'lj-gco ' + val + '\n' +
+        modify_obj_cmd(val + '->ffid', 242) + '\n'
+        'lj-gco ' + val + '\n' +
+        modify_obj_cmd(val + '->ffid', 253) + '\n'
+        'lj-gco ' + val + '\n'
+    )
+
+    lua_script = 'print(pcall)'
+
+    pattern = (
+        r'fast function pcall \(#[0-9]+\)\n'
+        r'fast function unknown \(#242\)\n'
+        r'fast function unknown \(#253\)\n'
+    )
+
+
+class TestLJFuncFFKnown(TestCaseBase):
+    location = 'lj_cf_print'
+    extension_cmds = ''.join(
+        ('lj-func ' + gcval('L->base + {}'.format(i)) + '\n'
+         for i in range(FFUNC_ARGS.count('\n')))
+    )
+
+    lua_script = (
+        'print(\n' +
+        FFUNC_ARGS +
+        '  1\n'  # Stub for the pattern.
+        ')\n'
+    )
+
+    pattern = FFUNC_RX
+
+
+class TestLJFuncFFUnknown(TestCaseBase):
+    location = 'lj_cf_print'
+
+    val = '((GCfuncC *)' + gcval('L->base') + ')'
+    extension_cmds = (
+        'lj-func ' + val + '\n' +
+        modify_obj_cmd(val + '->ffid', 242) + '\n'
+        'lj-func ' + val + '\n' +
+        modify_obj_cmd(val + '->ffid', 253) + '\n'
+        'lj-func ' + val + '\n'
+    )
+
+    lua_script = 'print(pcall)'
+
+    pattern = (
+        r'fast function pcall \(#[0-9]+\)\n'
+        r'fast function unknown \(#242\)\n'
+        r'fast function unknown \(#253\)\n'
+    )
+
+
 PROTO_FUNC_SCRIPT = (
     'local uvname = false\n'
     'local function testf(...)\n'
-- 
2.55.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [Tarantool-patches] [PATCH luajit 4/4] dbg: remove wrong GC state
  2026-11-12 23:42 [Tarantool-patches] [PATCH luajit 0/4] dbg: avoid hardcoded enums Mikhail Elhimov via Tarantool-patches
                   ` (2 preceding siblings ...)
  2026-11-12 23:42 ` [Tarantool-patches] [PATCH luajit 3/4] dbg: display fast function name along with ffid Mikhail Elhimov via Tarantool-patches
@ 2026-11-12 23:42 ` Mikhail Elhimov via Tarantool-patches
  3 siblings, 0 replies; 5+ messages in thread
From: Mikhail Elhimov via Tarantool-patches @ 2026-11-12 23:42 UTC (permalink / raw)
  To: Sergey Kaplun, Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches

It was copied originally by mistake from the LuaVela fork of LuaJIT.
---
 src/luajit_dbg.py                             |   3 +-
 .../debug-extension-tests.py                  | 113 ++++++++++++++++--
 2 files changed, 104 insertions(+), 12 deletions(-)

diff --git a/src/luajit_dbg.py b/src/luajit_dbg.py
index db4b3f04..1c2240f4 100644
--- a/src/luajit_dbg.py
+++ b/src/luajit_dbg.py
@@ -1302,7 +1302,6 @@ def gc_state(g):
         3: 'SWEEPSTRING',
         4: 'SWEEP',
         5: 'FINALIZE',
-        6: 'LAST',
     }.get(int(g['gc']['state']), 'INVALID')
 
 
@@ -2908,7 +2907,7 @@ class LJState(dbg.LJBase):
 lj-state
 The command requires no args and dumps current VM and GC states:
 * VM state: <{}>
-* GC state: <PAUSE|PROPAGATE|ATOMIC|SWEEPSTRING|SWEEP|FINALIZE|LAST>
+* GC state: <PAUSE|PROPAGATE|ATOMIC|SWEEPSTRING|SWEEP|FINALIZE>
 * JIT state: <IDLE|ACTIVE|RECORD|START|END|ASM|ERR>
     '''.format('|'.join(list(VM_STATES) + [vm_state_trace('TRACENO')]))
 
diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py
index 9237d3f1..172ec51b 100644
--- a/test/tarantool-debugger-tests/debug-extension-tests.py
+++ b/test/tarantool-debugger-tests/debug-extension-tests.py
@@ -163,6 +163,23 @@ elif machine == 'arm64' or machine == 'aarch64':
 else:
     raise Exception('Unknown architecture in testing')
 
+GCS_PAUSE = 'PAUSE'
+GCS_PROPAGATE = 'PROPAGATE'
+GCS_ATOMIC = 'ATOMIC'
+GCS_SWEEPSTRING = 'SWEEPSTRING'
+GCS_SWEEP = 'SWEEP'
+GCS_FINALIZE = 'FINALIZE'
+
+RX_VALID_GC_STATES = r'({})'.format('|'.join([
+    GCS_PAUSE,
+    GCS_PROPAGATE,
+    GCS_ATOMIC,
+    GCS_SWEEPSTRING,
+    GCS_SWEEP,
+    GCS_FINALIZE,
+]))
+RX_INVALID_GC_STATE = r'INVALID'
+
 
 class TestCaseBase(unittest.TestCase):
     @classmethod
@@ -309,23 +326,24 @@ RX_VM_STATE = r'({})'.format('|'.join([
 ]))
 
 
-class TestLJState(TestCaseBase):
-    extension_cmds = 'lj-state'
-    location = 'lj_cf_print'
-    lua_script = 'print(1)'
-    pattern = (
+def compose_lj_state_rx(rx_gc_state):
+    return (
         r'VM state: ' + RX_VM_STATE + '\n'
-        r'GC state: [A-Z]+\n'
+        r'GC state: ' + rx_gc_state + '\n'
         r'JIT state: [A-Z]+\n'
     )
 
 
-class TestLJGC(TestCaseBase):
-    extension_cmds = 'lj-gc'
+class TestLJState(TestCaseBase):
+    extension_cmds = 'lj-state'
     location = 'lj_cf_print'
     lua_script = 'print(1)'
-    pattern = (
-        r'GC stats: [A-Z]+\n'
+    pattern = compose_lj_state_rx(RX_VALID_GC_STATES)
+
+
+def compose_gc_stats_rx(rx_gc_state):
+    return (
+        r'GC stats: ' + rx_gc_state + '\n'
         r'\ttotal: \d+\n'
         r'\tthreshold: \d+\n'
         r'\tdebt: \d+\n'
@@ -341,6 +359,81 @@ class TestLJGC(TestCaseBase):
     )
 
 
+class TestLJGC(TestCaseBase):
+    extension_cmds = 'lj-gc'
+    location = 'lj_cf_print'
+    lua_script = 'print(1)'
+    pattern = compose_gc_stats_rx(RX_VALID_GC_STATES)
+
+
+class TestGCStateBase(TestCaseBase):
+    extension_cmds = (
+        'lj-state\n'
+        'lj-gc\n'
+    )
+
+    # The same lua code is used to check all the GC states because
+    # it allows to reach any of them:
+    # 1) `gc_mark_start()` (PAUSE) is always present in the full
+    #    GC cycle.
+    # 2) `propagatemark()` (PROPAGATE) is reachable because our
+    #    build has FFI and the finalizer table is marked as a weak
+    #    table, i.e. it is gray. Hence, the g->gc.gray list is not
+    #    empty.
+    # 3) `atomic()` (ATOMIC) is always present in the full GC
+    #    cycle.
+    # 4) `gc_sweep_str_chain()` (SWEEPSTRING) is always present in
+    #    the full GC cycle.
+    # 5) `gc_sweep()` (SWEEP) is always present in the full GC
+    #    cycle.
+    # 6) `gc_finalize()` (FINALIZE) is met due to the finalizer to
+    #    userdata in the script.
+    lua_script = (
+        'local obj = newproxy(true)\n'
+        'getmetatable(obj).__gc = function() end\n'
+        'obj = nil\n'
+        'collectgarbage()\n'
+    )
+
+    @classmethod
+    def setUpClass(cls):
+        cls.pattern = (
+            compose_lj_state_rx(cls.gc_state) +
+            compose_gc_stats_rx(cls.gc_state)
+        )
+        super(TestGCStateBase, cls).setUpClass()
+
+
+class TestGCStatePause(TestGCStateBase):
+    gc_state = GCS_PAUSE
+    location = 'gc_mark_start'
+
+
+class TestGCStatePropagate(TestGCStateBase):
+    gc_state = GCS_PROPAGATE
+    location = 'propagatemark'
+
+
+class TestGCStateAtomic(TestGCStateBase):
+    gc_state = GCS_ATOMIC
+    location = 'atomic'
+
+
+class TestGCStateSweepString(TestGCStateBase):
+    gc_state = GCS_SWEEPSTRING
+    location = 'gc_sweep_str_chain'
+
+
+class TestGCStateSweep(TestGCStateBase):
+    gc_state = GCS_SWEEP
+    location = 'gc_sweep'
+
+
+class TestGCStateFinalize(TestGCStateBase):
+    gc_state = GCS_FINALIZE
+    location = 'gc_finalize'
+
+
 STACK_RX = (
     r'-+ Red zone:\s+\d+ slots -+\n'
     r'(' + RX_ADDR + r'\s+' + RX_FRAME + r' VALUE: nil\n?)*\n'
-- 
2.55.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-11-12 23:44 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-12 23:42 [Tarantool-patches] [PATCH luajit 0/4] dbg: avoid hardcoded enums Mikhail Elhimov via Tarantool-patches
2026-11-12 23:42 ` [Tarantool-patches] [PATCH luajit 1/4] dbg: clean up and unification Mikhail Elhimov via Tarantool-patches
2026-11-12 23:42 ` [Tarantool-patches] [PATCH luajit 2/4] dbg: avoid hardcoded enums Mikhail Elhimov via Tarantool-patches
2026-11-12 23:42 ` [Tarantool-patches] [PATCH luajit 3/4] dbg: display fast function name along with ffid Mikhail Elhimov via Tarantool-patches
2026-11-12 23:42 ` [Tarantool-patches] [PATCH luajit 4/4] dbg: remove wrong GC state Mikhail Elhimov 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