Ask to ocaml compiler experts: Why some functions lost it's debug event in bytecode

I have made an vscode debugger adapter named ocamlearlybird. Almost finish. but I encountered a wired bug:

CamlinternalLazy.force_lazy_block has debug event. But it’s caller CamlinternalLazy.force hasn’t.

OCaml version: 4.11.1
Test file:

let rec main () =
  let lazy' = Lazy.from_fun (fun () -> 100) in
  let _ = Lazy.force lazy' in
  print_endline "hello";
  print_endline "hello";
;;
main ();

I want to know why.

I am not sure (I am not familiar with the placement of debug events), but:

  1. In most settings, Lazy.force does not call CamlinternalLazy.force, it is interpreted directly as a compiler primitive; are you sure that CamlinternalLazy.force is called in your test?
  2. The compiler code (today and also in 4.11) should insert a debug event “after” Lazy.force.
  3. Your code is a bit strange because Lazy.from_fun (fun () -> 100) is more verbose and less efficient than just lazy 100; this should not affect the part that surprises you, of course.

Thank you!

Your answer can explain why StepIn Lazy.force directly go into CamlinternalLazy.force_lazy_block. And after the last statement of CamlinternalLazy.force_lazy_block executed. It returns just after Lazy.force statement.

So the question now changed to: Why UP_FRAME debugger command inside CamlinternalLazy.force_lazy_block results a (stack_pos, pc) which can’t find any event at pc? This makes stack walk can not access frames under top frame.

I may found why. I see compiler expand Lazy.force as inlined version of CamlinternalLazy.force. So the return point of CamlinternalLazy.force_lazy_block is at middle of inlined code. And no debug event at there.

So this break my implementation. I must guess the correct return point of virtual call to Lazy.force. On this case, It just placed after the pc not far. But it’s not guaranteed on all cases.

@gasche Is there an compiler cli option to disable such inline optimization? Looks like there’s Clflags.afl_instrument. How to make the test code to compile with afl_instrument?

Again, I’m not familiar with debug events. A more knowledgeable person might chime in to comment, or you may want to open an issue on the compiler codebase to get more answers. (Please make sure to include a clear description of what you are trying to do and what you expect.) Below is my best-effort attempt at saying a bit more.

One thing that would help is understand what you are trying to do. I suspect it is “walk up the call stack using the debugger protocol, and expect to see all things that logically look like function calls to the user”. It may be that “yeah this is actually not a function call, so it is not a frame on the call stack and the debugger will skip it” is the best current answer, I am not sure. Ideally there should be a way to keep information on “virtual” calls that have been inlined, but I don’t know that such a mechanism exist today. (At first I thought that this part of primitive translation was doing that, but as I read more stuff I understand that this looks more related to the ability to put breakpoints after the primitive returned).

Note that there are other primitives that result in code generation instead of a function call, Printexc.raise_with_backtrace for example.

Finally: while investigating your issue I noticed that there is a discrepancy between native and bytecode backtraces involving Lazy.force in 4.12 development version (there was a native-code improvement that does not affect the bytecode, it seems), I asked about this in https://github.com/ocaml/ocaml/pull/9469#issuecomment-757495542.

1 Like