Skip to main content

luau_vm/thread/
debug.rs

1use core::mem::size_of;
2use core::ptr::{self, NonNull};
3
4use luau_bytecode::model::Instruction;
5use luau_bytecode::opcodes::Opcode;
6use luau_common::{BStr, BString, ByteSlice};
7
8use super::stack::RawStackAccess;
9use super::{LuaStringBuilder, LuaStringBuilderStorage, Thread};
10use crate::VmErrorResult;
11use crate::debug::{
12    DebugRuntime, LuaCounterFunction, LuaCounterValue, LuaCoverage, LuaDebug, chunk_id, current_pc,
13    lua_proto,
14};
15use crate::function::{Closure, Proto};
16use crate::gc::GcBarrier;
17use crate::handle::RawHandle;
18use crate::memory::MemoryRuntime;
19use crate::state::{CallInfo, RawCallInfo, ThreadState};
20use crate::string::LuaString;
21use crate::value::TValue;
22use crate::vm::VmCallFrame;
23
24// Debug inspection
25fn get_func_name(closure: Closure) -> Option<LuaString> {
26    if unsafe { closure.is_native() } {
27        unsafe { closure.native_debug_name() }
28    } else {
29        let proto = unsafe { closure.proto().unwrap_unchecked() };
30        unsafe { proto.debug_name().map(LuaString::from_interned) }
31    }
32}
33
34/// `auxgetinfo`
35fn aux_get_info(
36    what: &[u8],
37    ar: &mut LuaDebug,
38    closure: Closure,
39    call_info: Option<*mut RawCallInfo>,
40) -> bool {
41    let mut push_function = false;
42
43    for byte in what {
44        match *byte {
45            b's' => {
46                if unsafe { closure.is_native() } {
47                    ar.source = LuaString::from_static(b"=[C]".as_bstr());
48                    ar.what = LuaString::from_static(b"C".as_bstr());
49                    ar.linedefined = -1;
50                    ar.set_short_src(b"[C]");
51                } else {
52                    let proto = unsafe { closure.proto().unwrap_unchecked() };
53                    let source = unsafe { proto.source().unwrap_unchecked() };
54                    ar.source = LuaString::from_interned(source);
55                    ar.what = LuaString::from_static(b"Lua".as_bstr());
56                    ar.linedefined =
57                        unsafe { proto.as_ptr().as_ref().unwrap_unchecked().line_defined };
58
59                    let mut short_src = [0u8; crate::debug::LUA_ID_SIZE];
60                    let short_src = chunk_id(&mut short_src, unsafe { source.as_bytes() });
61                    ar.set_short_src(short_src);
62                }
63            }
64            b'l' => {
65                ar.currentline = if let Some(call_info) = call_info {
66                    let call_info =
67                        unsafe { CallInfo::from_raw(NonNull::new_unchecked(call_info)) };
68                    if let Some(proto) = lua_proto(call_info) {
69                        unsafe { proto.get_line(current_pc(call_info, proto)) }
70                    } else {
71                        -1
72                    }
73                } else if unsafe { closure.is_native() } {
74                    -1
75                } else {
76                    unsafe {
77                        closure
78                            .proto()
79                            .unwrap_unchecked()
80                            .as_ptr()
81                            .as_ref()
82                            .unwrap_unchecked()
83                            .line_defined
84                    }
85                };
86            }
87            b'u' => {
88                ar.nupvals = unsafe { closure.as_ptr().as_ref().unwrap_unchecked().n_upvalues };
89            }
90            b'a' => {
91                if unsafe { closure.is_native() } {
92                    ar.is_vararg = true;
93                    ar.nparams = 0;
94                } else {
95                    let proto = unsafe { closure.proto().unwrap_unchecked() };
96                    let proto = unsafe { proto.as_ptr().as_ref().unwrap_unchecked() };
97                    ar.is_vararg = proto.is_vararg != 0;
98                    ar.nparams = proto.num_params;
99                }
100            }
101            b'n' => {
102                ar.name = get_func_name(closure);
103            }
104            b'f' => {
105                push_function = true;
106            }
107            _ => {}
108        }
109    }
110
111    push_function
112}
113
114/// `aux_upvalue`
115fn aux_upvalue(function: TValue, upvalue_index: i32) -> Option<(LuaString, TValue)> {
116    if !function.is_function() {
117        return None;
118    }
119
120    let closure = function.closure_value();
121    if unsafe { closure.is_native() } {
122        if !(1..=i32::from(unsafe { closure.as_ptr().as_ref().unwrap_unchecked().n_upvalues }))
123            .contains(&upvalue_index)
124        {
125            return None;
126        }
127
128        let value = unsafe { closure.native_upvalue((upvalue_index - 1) as usize) };
129        return Some((LuaString::from_static(b"".as_bstr()), value));
130    }
131
132    let proto = unsafe { closure.proto().unwrap_unchecked() };
133    if !(1..=i32::from(unsafe { proto.as_ptr().as_ref().unwrap_unchecked().n_ups }))
134        .contains(&upvalue_index)
135    {
136        return None;
137    }
138
139    let upref = unsafe { closure.lua_upvalue_ref((upvalue_index - 1) as usize) };
140    let value = if upref.is_upvalue() {
141        unsafe { upref.upvalue_value().value() }
142    } else {
143        upref
144    };
145
146    let name = if let Some(name) = unsafe { proto.upvalue_name((upvalue_index - 1) as usize) } {
147        LuaString::from_interned(name)
148    } else {
149        LuaString::from_static(b"".as_bstr())
150    };
151
152    Some((name, value))
153}
154
155/// `append`
156fn append_limited(output: &mut BString, data: &[u8], limit: usize) {
157    if output.len() >= limit {
158        return;
159    }
160
161    let remaining = limit - output.len();
162    output.extend_from_slice(&data[..data.len().min(remaining)]);
163}
164
165/// `getcounters`
166fn get_counters(
167    thread: &Thread,
168    proto: Proto,
169    context: *mut (),
170    function_visit: LuaCounterFunction,
171    counter_visit: LuaCounterValue,
172) {
173    let get_counter_data = unsafe { thread.global() }.execution_counter_data();
174    let proto_ref = unsafe { proto.as_ptr().as_ref().unwrap_unchecked() };
175    if !proto_ref.exec_data.is_null()
176        && let Some(get_counter_data) = get_counter_data
177    {
178        let mut count = 0usize;
179        let mut data = unsafe { get_counter_data(thread, proto, &mut count) };
180
181        if !data.is_null() && count != 0 {
182            let debug_name_string = unsafe { proto.debug_name() };
183            let debug_name = debug_name_string
184                .as_ref()
185                .map(|string| unsafe { string.as_bytes().as_bstr() });
186            let line_defined = proto_ref.line_defined;
187
188            function_visit(context, debug_name, line_defined);
189
190            for _ in 0..count {
191                let kind = unsafe { ptr::read_unaligned(data.cast::<u32>()) } as i32;
192                data = unsafe { data.add(size_of::<u32>()) };
193
194                let pc_pos = unsafe { ptr::read_unaligned(data.cast::<u32>()) };
195                data = unsafe { data.add(size_of::<u32>()) };
196
197                let hits = unsafe { ptr::read_unaligned(data.cast::<u64>()) };
198                data = unsafe { data.add(size_of::<u64>()) };
199
200                let line = if pc_pos == u32::MAX {
201                    proto_ref.line_defined
202                } else {
203                    unsafe { proto.get_line(pc_pos as i32) }
204                };
205
206                counter_visit(context, kind, line, hits);
207            }
208        }
209    }
210
211    for index in 0..proto_ref.size_p as usize {
212        let child = unsafe { proto.child_proto(index).unwrap_unchecked() };
213        get_counters(thread, child, context, function_visit, counter_visit);
214    }
215}
216
217/// `getmaxline`
218fn get_max_line(proto: Proto) -> i32 {
219    let mut result = -1;
220
221    let proto_ref = unsafe { proto.as_ptr().as_ref().unwrap_unchecked() };
222    for index in 0..proto_ref.size_code as usize {
223        let line = unsafe { proto.get_line(index as i32) };
224        result = result.max(line);
225    }
226
227    for index in 0..proto_ref.size_p as usize {
228        let child = unsafe { proto.child_proto(index).unwrap_unchecked() };
229        result = result.max(get_max_line(child));
230    }
231
232    result
233}
234
235/// `getnextline`
236fn get_next_line(proto: Proto, line: i32) -> i32 {
237    let mut closest = -1;
238
239    let proto_ref = unsafe { proto.as_ptr().as_ref().unwrap_unchecked() };
240    if !proto_ref.line_info.is_null() {
241        for index in 0..proto_ref.size_code as usize {
242            let instruction = Instruction::new(unsafe { *proto_ref.code.add(index) });
243            if unsafe { instruction.opcode_unchecked() } == Opcode::PrepVarargs {
244                continue;
245            }
246
247            let candidate = unsafe { proto.get_line(index as i32) };
248            if candidate == line {
249                return line;
250            }
251
252            if candidate > line && (closest == -1 || candidate < closest) {
253                closest = candidate;
254            }
255        }
256    }
257
258    for index in 0..proto_ref.size_p as usize {
259        let child = unsafe { proto.child_proto(index).unwrap_unchecked() };
260        let candidate = get_next_line(child, line);
261        if candidate == line {
262            return line;
263        }
264
265        if candidate > line && (closest == -1 || candidate < closest) {
266            closest = candidate;
267        }
268    }
269
270    closest
271}
272
273/// `getcoverage`
274fn get_coverage_recursive(
275    proto: Proto,
276    depth: i32,
277    buffer: &mut [i32],
278    context: *mut (),
279    callback: LuaCoverage,
280) {
281    buffer.fill(-1);
282
283    let proto_ref = unsafe { proto.as_ptr().as_ref().unwrap_unchecked() };
284    for index in 0..proto_ref.size_code as usize {
285        let instruction = Instruction::new(unsafe { *proto_ref.code.add(index) });
286        if unsafe { instruction.opcode_unchecked() } != Opcode::Coverage {
287            continue;
288        }
289
290        let line = unsafe { proto.get_line(index as i32) };
291        let hits = instruction.e();
292        debug_assert!((line as usize) < buffer.len());
293
294        let entry = &mut buffer[line as usize];
295        *entry = (*entry).max(hits);
296    }
297
298    let debug_name_string = unsafe { proto.debug_name() };
299    let debug_name = debug_name_string
300        .as_ref()
301        .map(|string| unsafe { string.as_bytes().as_bstr() });
302    let line_defined = proto_ref.line_defined;
303    callback(context, debug_name, line_defined, depth, buffer);
304
305    for index in 0..proto_ref.size_p as usize {
306        let child = unsafe { proto.child_proto(index).unwrap_unchecked() };
307        get_coverage_recursive(child, depth + 1, buffer, context, callback);
308    }
309}
310
311impl Thread {
312    /// `lua_callhook`
313    pub unsafe fn call_hook<F>(&self, hook: F, userdata: *mut ()) -> crate::VmResult
314    where
315        F: FnOnce(&Thread, &mut LuaDebug) -> crate::VmResult,
316    {
317        debug_assert!(unsafe { self.current_call_info() != self.base_call_info() });
318        unsafe { <Self as VmCallFrame>::call_hook(self, hook, userdata) }
319    }
320
321    /// `lua_stackdepth`
322    pub unsafe fn stack_depth(&self) -> i32 {
323        unsafe {
324            self.current_call_info_cursor()
325                .offset_from(self.base_call_info_cursor()) as i32
326        }
327    }
328
329    /// `lua_getinfo`
330    pub unsafe fn get_info(&self, level: i32, what: &str, ar: &mut LuaDebug) -> VmErrorResult<i32> {
331        unsafe {
332            let mut closure = None;
333            let mut call_info = None;
334            let mut stack_function = None;
335
336            if level < 0 {
337                let available = self.stack_top().offset_from(self.stack_base()) as i32;
338                if -level > available {
339                    return Ok(0);
340                }
341
342                let function = self.stack_top().offset(level as isize);
343                if !function.value_unchecked().is_function() {
344                    return Ok(0);
345                }
346
347                stack_function = Some(function);
348                closure = Some(function.value_unchecked().closure_value());
349            } else if (level as usize)
350                < self
351                    .current_call_info_cursor()
352                    .offset_from(self.base_call_info_cursor()) as usize
353            {
354                let ci = self
355                    .current_call_info_cursor()
356                    .sub(level as usize)
357                    .call_info_unchecked();
358                call_info = Some(ci);
359                closure = Some(ci.function_closure());
360            }
361
362            let Some(closure) = closure else {
363                return Ok(0);
364            };
365
366            if aux_get_info(
367                what.as_bytes(),
368                ar,
369                closure,
370                call_info.map(|ci| ci.as_ptr()),
371            ) {
372                self.thread_barrier();
373
374                if let Some(call_info) = call_info {
375                    self.push_value_internal(call_info.function().value_unchecked())?;
376                } else {
377                    self.push_value_internal(stack_function.unwrap_unchecked().value_unchecked())?;
378                }
379            }
380
381            Ok(1)
382        }
383    }
384
385    /// `lua_getargument`
386    pub unsafe fn get_argument(&self, level: i32, argument: i32) -> VmErrorResult<i32> {
387        unsafe {
388            let depth = self.stack_depth();
389            if (level as u32) >= (depth as u32) {
390                return Ok(0);
391            }
392
393            let call_info = self
394                .current_call_info_cursor()
395                .sub(level as usize)
396                .call_info_unchecked();
397            if call_info.as_ptr().as_ref().unwrap_unchecked().flags
398                & crate::state::LUA_CALLINFO_NATIVE
399                != 0
400            {
401                return Ok(0);
402            }
403
404            let Some(proto) = lua_proto(call_info) else {
405                return Ok(0);
406            };
407
408            if argument <= 0 {
409                return Ok(0);
410            }
411
412            if argument <= i32::from(proto.as_ptr().as_ref().unwrap_unchecked().num_params) {
413                self.thread_barrier();
414                self.push_value_internal(
415                    call_info
416                        .base()
417                        .add((argument - 1) as usize)
418                        .value_unchecked(),
419                )?;
420                Ok(1)
421            } else if proto.as_ptr().as_ref().unwrap_unchecked().is_vararg != 0
422                && argument < call_info.base().offset_from(call_info.function()) as i32
423            {
424                self.thread_barrier();
425                self.push_value_internal(
426                    call_info
427                        .function()
428                        .add(argument as usize)
429                        .value_unchecked(),
430                )?;
431                Ok(1)
432            } else {
433                Ok(0)
434            }
435        }
436    }
437
438    /// `lua_getlocal`
439    pub unsafe fn get_local(&self, level: i32, local: i32) -> VmErrorResult<Option<LuaString>> {
440        unsafe {
441            let depth = self.stack_depth();
442            if (level as u32) >= (depth as u32) {
443                return Ok(None);
444            }
445
446            let call_info = self
447                .current_call_info_cursor()
448                .sub(level as usize)
449                .call_info_unchecked();
450            if call_info.as_ptr().as_ref().unwrap_unchecked().flags
451                & crate::state::LUA_CALLINFO_NATIVE
452                != 0
453            {
454                return Ok(None);
455            }
456
457            let Some(proto) = lua_proto(call_info) else {
458                return Ok(None);
459            };
460            let Some(var) = proto.get_local(local, current_pc(call_info, proto)) else {
461                return Ok(None);
462            };
463
464            self.thread_barrier();
465            self.push_value_internal(
466                call_info
467                    .base()
468                    .add(var.as_ptr().as_ref().unwrap_unchecked().reg as usize)
469                    .value_unchecked(),
470            )?;
471
472            let name = var.name();
473            debug_assert!(name.is_some());
474            Ok(Some(LuaString::from_interned(name.unwrap_unchecked())))
475        }
476    }
477
478    /// `lua_setlocal`
479    pub unsafe fn set_local(&self, level: i32, local: i32) -> Option<LuaString> {
480        unsafe {
481            debug_assert!(self.stack_top() > self.stack_base());
482
483            let depth = self.stack_depth();
484            if (level as u32) >= (depth as u32) {
485                return None;
486            }
487
488            let call_info = self
489                .current_call_info_cursor()
490                .sub(level as usize)
491                .call_info_unchecked();
492            if call_info.as_ptr().as_ref().unwrap_unchecked().flags
493                & crate::state::LUA_CALLINFO_NATIVE
494                != 0
495            {
496                return None;
497            }
498
499            let proto = lua_proto(call_info)?;
500            let var = proto.get_local(local, current_pc(call_info, proto))?;
501            call_info
502                .base()
503                .add(var.as_ptr().as_ref().unwrap_unchecked().reg as usize)
504                .value_unchecked()
505                .set_obj(self.stack_top().sub(1).value_unchecked());
506            self.set_stack_top(self.stack_top().sub(1));
507
508            let name = var.name();
509            debug_assert!(name.is_some());
510            Some(LuaString::from_interned(name.unwrap_unchecked()))
511        }
512    }
513
514    /// `lua_getupvalue`
515    pub unsafe fn get_upvalue(
516        &self,
517        function_index: i32,
518        upvalue_index: i32,
519    ) -> VmErrorResult<Option<LuaString>> {
520        unsafe {
521            self.thread_barrier();
522            self.ensure_stack(self, 1)?;
523
524            let function = self.to_object(function_index).unwrap_unchecked();
525            let Some((name, value)) = aux_upvalue(function, upvalue_index) else {
526                return Ok(None);
527            };
528
529            self.push_value_internal(value)?;
530            Ok(Some(name))
531        }
532    }
533
534    /// `lua_setupvalue`
535    pub unsafe fn set_upvalue(&self, function_index: i32, upvalue_index: i32) -> Option<LuaString> {
536        unsafe {
537            let function = self.to_object(function_index).unwrap_unchecked();
538            let (name, value) = aux_upvalue(function, upvalue_index)?;
539
540            self.set_stack_top(self.stack_top().sub(1));
541            value.set_obj(self.stack_top().value_unchecked());
542
543            let closure = function.closure_value();
544            let written = self.stack_top().value_unchecked();
545            if written.is_collectable() {
546                let object = closure.into();
547                let child = written.gc_value();
548                self.barrier_forward(object, child);
549            }
550
551            Some(name)
552        }
553    }
554
555    /// `lua_singlestep`
556    pub unsafe fn single_step(&self, enabled: i32) {
557        unsafe { self.as_ptr().as_mut().unwrap_unchecked().single_step = enabled != 0 };
558    }
559
560    /// `lua_breakpoint`
561    pub unsafe fn breakpoint(
562        &self,
563        function_index: i32,
564        line: i32,
565        enabled: i32,
566    ) -> VmErrorResult<i32> {
567        unsafe {
568            let function = self.to_object(function_index).unwrap_unchecked();
569            debug_assert!(function.is_function());
570
571            let closure = function.closure_value();
572            debug_assert!(closure.is_lua());
573
574            let proto = closure.proto().unwrap_unchecked();
575
576            let target = get_next_line(proto, line);
577            if target != -1 {
578                self.breakpoint_internal(proto, target, enabled != 0)?;
579            }
580
581            Ok(target)
582        }
583    }
584
585    /// `lua_hascustomexecution`
586    pub unsafe fn has_custom_execution(&self, level: i32) -> i32 {
587        unsafe { self.has_native(level) }
588    }
589
590    /// `lua_incustomexecution`
591    pub unsafe fn in_custom_execution(&self, level: i32) -> i32 {
592        unsafe { self.is_native(level) }
593    }
594
595    /// `lua_atbreakpoint`
596    pub unsafe fn at_breakpoint(&self) -> i32 {
597        i32::from(unsafe { self.on_break() })
598    }
599
600    /// `lua_getcoverage`
601    pub unsafe fn get_coverage(
602        &self,
603        function_index: i32,
604        context: *mut (),
605        callback: LuaCoverage,
606    ) -> VmErrorResult {
607        unsafe {
608            let function = self.to_object(function_index).unwrap_unchecked();
609            debug_assert!(function.is_function());
610
611            let closure = function.closure_value();
612            debug_assert!(closure.is_lua());
613
614            let proto = closure.proto().unwrap_unchecked();
615
616            let size = get_max_line(proto) + 1;
617            if size <= 0 {
618                return Ok(());
619            }
620
621            let buffer = self.new_array::<i32>(size as usize, 0)?;
622            let buffer = core::slice::from_raw_parts_mut(buffer, size as usize);
623
624            get_coverage_recursive(proto, 0, buffer, context, callback);
625
626            self.free_array(buffer.as_mut_ptr(), size as usize, 0);
627        }
628        Ok(())
629    }
630
631    /// `lua_getcounters`
632    pub unsafe fn get_counters(
633        &self,
634        function_index: i32,
635        context: *mut (),
636        function_visit: LuaCounterFunction,
637        counter_visit: LuaCounterValue,
638    ) {
639        unsafe {
640            let Some(function) = self.to_object(function_index) else {
641                return;
642            };
643            if !function.is_function() {
644                return;
645            }
646
647            if self.global().execution_counter_data().is_none() {
648                return;
649            }
650
651            let closure = function.closure_value();
652            if closure.is_native() {
653                return;
654            }
655
656            let proto = closure.proto().unwrap_unchecked();
657            get_counters(self, proto, context, function_visit, counter_visit);
658        }
659    }
660
661    /// `lua_debugtrace`
662    pub unsafe fn debug_trace(&self) -> VmErrorResult<BString> {
663        const BUFFER_SIZE: usize = 4096;
664        const LIMIT_1: i32 = 10;
665        const LIMIT_2: i32 = 10;
666        const BYTE_LIMIT: usize = BUFFER_SIZE - 1;
667
668        unsafe {
669            let depth = self.stack_depth();
670            let mut output = BString::new(Vec::new());
671            let mut ar = LuaDebug::default();
672            let mut level = 0;
673
674            while self.get_info(level, "sln", &mut ar)? != 0 {
675                if !ar.source.as_bytes().is_empty() {
676                    append_limited(&mut output, ar.short_src(), BYTE_LIMIT);
677                }
678
679                if ar.currentline > 0 {
680                    append_limited(&mut output, b":", BYTE_LIMIT);
681                    let mut line = [0u8; crate::number::LUAI_MAXINT2STR];
682                    let len = crate::number::int_to_str(&mut line, i64::from(ar.currentline));
683                    append_limited(&mut output, &line[..len], BYTE_LIMIT);
684                }
685
686                if let Some(name) = ar.name {
687                    append_limited(&mut output, b" function ", BYTE_LIMIT);
688                    append_limited(&mut output, name.as_bytes(), BYTE_LIMIT);
689                }
690
691                append_limited(&mut output, b"\n", BYTE_LIMIT);
692
693                if depth > LIMIT_1 + LIMIT_2 && level == LIMIT_1 - 1 {
694                    append_limited(&mut output, b"... (+", BYTE_LIMIT);
695                    let mut skipped = [0u8; crate::number::LUAI_MAXINT2STR];
696                    let skipped_frames = i64::from(depth - LIMIT_1 - LIMIT_2);
697                    let len = crate::number::int_to_str(&mut skipped, skipped_frames);
698                    append_limited(&mut output, &skipped[..len], BYTE_LIMIT);
699                    append_limited(&mut output, b" frames)\n", BYTE_LIMIT);
700                    level = depth - LIMIT_2 - 1;
701                }
702
703                level += 1;
704                ar = LuaDebug::default();
705            }
706
707            Ok(output)
708        }
709    }
710}
711
712// Traceback construction
713impl Thread {
714    /// `luaL_traceback`
715    pub unsafe fn traceback(
716        &self,
717        source: Option<&Thread>,
718        message: Option<&BStr>,
719        level: i32,
720    ) -> VmErrorResult {
721        debug_assert!(level >= 0);
722
723        let source = source.unwrap_or(self);
724        unsafe {
725            let mut buffer_storage = LuaStringBuilderStorage::uninit();
726            let mut buffer = LuaStringBuilder::new(self, &mut buffer_storage);
727
728            if let Some(message) = message {
729                buffer.push_bytes(message.as_bytes())?;
730                buffer.push_byte(b'\n')?;
731            }
732
733            let mut frame = level;
734            let mut ar = LuaDebug::default();
735            while source.get_info(frame, "sln", &mut ar)? != 0 {
736                frame += 1;
737
738                if ar.what.as_bytes() == b"C" {
739                    ar = LuaDebug::default();
740                    continue;
741                }
742
743                if !ar.source.as_bytes().is_empty() {
744                    buffer.push_bytes(ar.short_src())?;
745                }
746
747                let mut line_digits = None;
748                if ar.currentline > 0 {
749                    let mut digits = [0u8; 32];
750                    let mut end = digits.len();
751                    let mut line = ar.currentline as u32;
752                    while line > 0 {
753                        end -= 1;
754                        digits[end] = b'0' + (line % 10) as u8;
755                        line /= 10;
756                    }
757                    line_digits = Some((digits, end));
758                }
759
760                if let Some((digits, end)) = line_digits {
761                    buffer.push_byte(b':')?;
762                    buffer.push_bytes(&digits[end..])?;
763                }
764
765                if let Some(name) = ar.name {
766                    buffer.push_bytes(b" function ")?;
767                    buffer.push_bytes(name.as_bytes())?;
768                }
769
770                buffer.push_byte(b'\n')?;
771                ar = LuaDebug::default();
772            }
773
774            buffer.finish()?;
775        }
776        Ok(())
777    }
778}