Skip to content

Debugger Protocol

Simon Krajewski edited this page Sep 20, 2018 · 1 revision

The new haxe interpreter target, called eval, that is now used for macros and --interp runs has debugging support built-in. This page describes the protocol used by the debugger so a tool author can implement a frontend for it.

Connection

Debugger interacts with the frontend by connecting to a TCP port supplied through a compiler define flag eval-debugger. The value format for the flag is <host>:<port> (for example, haxe build.hxml -D eval-debugger=127.0.0.1:6666). The frontend must open a listening socket at this address, launch haxe with specified arguments and wait for eval debugger to connect. After connection is established, a two-way message exchange between debugger and frontend is active.

[ebishton: We shouldn't rely simply on a socket connection being established. We probably want to specify a connect packet format, including a protocol version number, to ensure that we're talking to a debugger and not some random thing listening on the socket, and an ACK packet, with a (list of?) protocol version supported. ]

Packet format

Frontend and debugger send and receive messages to each other using the following simple packet format: <len><body>, where <len> is the byte length of the following <body>, encoded as 32-bit little-endian unsigned integer. The <body> is a JSON-encoded message, encoded to bytes using UTF-8 encoding. So <len> represents the number of bytes of a utf-8 encoded json string of the <body>.

[nicolas : Requiring UTF-8 is also a PITA if we want to send binary data. if the protocol is binary it should use <len:32 bits>body:binary]

Base protocol

The protocol is based on the JSON-RPC 2.0 with the following considerations:

  • batch requests are not supported

Commands

[ebishton: We are also missing thread commands: list. We can probably resist adding others by putting adding thread IDs to the other commands (like Get stack trace, continue, step, etc.). ]

Breakpoints

Set breakpoint

Name: setBreakpoint

Params: {file:String, line:Int, ?column:Int, ?condition:String}. Line and column are 1-based. If column is omitted, the interpreter will stop at the first expression on given line. If condition is set, the interpreter will stop only if it evaluates to true.

Result: {id:Int}. Returns id of a created breakpoint.

Remove breakpoint

Name: removeBreakpoint

Argument: {id:Int} (breakpoint id returned by the "set breakpoint" command).

Result: null

[ebishton: Need a "list breakpoints" command.]

Set breakpoints for file

Name: setBreakpoints

Argument: {file:String, breakpoints:Array<{line:Int, ?column:Int}>}.

Result: Array<{id:Int}> (array of the breakpoint ids in the same order as in the argument)

This command replaces all breakpoints for a given file in a single command, clearing all old breakpoints and setting new ones with given coordinates. So, to clear breakpoints for a file, one should pass empty array.

This is useful for editors that manage breakpoint lists themselves, like VS Code.

Set function breakpoints

Name: setFunctionBreakpoints

Argument: [{breakpoint:{name:String}}]

This command clears the current function breakpoints and replaces them with the ones described in the argument. The name is expected to have the form Type.field or package.Type.field. While such a breakpoint is active, execution will stop whenever the function denoted by the argument is entered.

Result: same as setBreakpoints

Stepping

Continue

Name: continue

Argument: none

Result: null

Step over (next)

Name: next

Argument: none

Result: null

[ebishton: Perhaps the result from step over/in/out should include the top if the stack, given that each step is implicitly a breakpoint, and the first thing that any GUI is going to do is request the stack state and the top frame. ]

Step in

Name: stepIn

Argument: none

Result: null

Step out

Name: stepOut

Argument: none

Result: null

[ebishton: Need a "halt" or "pause" command. ]

[ebishton: Maybe "kill" and "restart" commands? ]

Stack & scope information

Get stacktrace

Name: stackTrace

Argument: none

Result: Array<StackFrameInfo>. Array of stack frame information objects with the following structure:

typedef StackFrameInfo = {
	var id:Int;
	var name:String;
	var source:String;
	var line:Int;
	var column:Int;
	var endLine:Int;
	var endColumn:Int;
	var artificial:Bool;
}

[ebishton: Also need to deal with incorrect state. For example, a stack is requested but the app/thread is not stopped. ]

Switch current frame

Name: switchFrame

Argument: {id:Int} (id, the value of the field id in the StackFrameInfo object described above)

Result: null

When stop happens, the debugger is always at frame 0, so explicit switching to it is not necessary.

[ebishton: I'm not sure why the debug server needs to track this state. The GUI/CLI can keep track of this by itself. If a particular stack frame is needed in order to inspect variables, then I would expect that frame ID to be included in the "evaluate expression" command. Oh, yeah. We need one of those, too. ]

Get variable scopes

Name: getScopes

Argument: none

Result: Array<ScopeInfo>. Array of objects describing non-empty variable scopes for the current frame.

/** Info about a scope **/
typedef ScopeInfo = {
	/** Scope identifier to use for the `vars` request. **/
	var id:Int;
	/** Name of the scope (e.g. Locals, Captures, etc) **/
	var name:String;
	/** Position information about scope boundaries, if present **/
	@:optional var pos:{source:String, line:Int, column:Int, endLine:Int, endColumn:Int};
}

Scopes are created by the eval interpreter for each function and block expression. This function returns only ones containing variables.

Get scope variables

Name: getScopeVariables

Argument: {id:Int} (the id of a variable scope within current frame, returned in the id field of the scopes request described above)

Result: Array<VarInfo>. Array of variable info objects with the following structure:

typedef VarInfo = {
	/** Variable/field name, for array elements or enum ctor arguments looks like `[0]` **/
	var name:String;
	/** Value type **/
	var type:String;
	/** Current value to display (structured child values are rendered with `...`) **/
	var value:String;
	/** True if this variable is structured, meaning that we can request "subvariables" (fields/elements) **/
	var structured:Bool;
	/** Access expression used to reference this variable.
	    For scope-level vars it's the same as name, for child vars it's an expression like `a.b[0].c[1]`.
	**/
	var access:String;
}

Get the inner structure of a variable

Name: getStructure

Argument: {expr:String} (an expression pointing to a specific variable as returned in the access field of the VarInfo object described above.

Result: Array<VarInfo>. Array of variable info objects with the structure described as the VarInfo object above.

This effectively allows getting the inner structure of a local variable at any level by supplying proper path.

Values

Evaluate

Name: evaluate

Argument: {expr:String}

Result: VarInfo. Object describing the evaluated value.

Parses the provided expression and interprets it as a value. If successful, a string representation of that value is returned.

Set value

Name: setVariable

Argument: {expr:String, value:String}, where expr is an expression pointing to a specific variable as returned in the access field of the VarInfo object described above, and value is a value expression supported by Haxe.

Result: VarInfo. Object describing this variable after the successful change.

Completion

Name: getCompletion

Argument: {text:String, column:String}

Result: Array<{label:String, kind:String, ?start:Int}>

Parses the given text while assuming that the display position is at the given column. If successful, the expression is converted to a value (like in evaluate) and the output is a list of information applicable to the value:

  • For identifiers, a list of all available identifiers is returned.
  • For dot access completion (expr.|), a list of available fields is returned.
  • In an array access expression (expr[|]), the available indices are returned.

If the expression cannot be parsed, an error "No completion point found" is returned. If it can be parsed, but there is no meaningful information for the resulting value available, an empty array is returned.

Misc

Set exception options

Name: setExceptionOptions

Argument: Array<String>

Result: null

Sets the options for how to handle exceptions:

  • If the argument contains "all", then all exceptions cause execution to halt.
  • Otherwise, if the argument contains "uncaught", only exceptions which are not caught cause execution to halt.
  • Otherwise exceptions do not halt execution.

Events

Breakpoint stop

Name: breakpointStop

Result: none

Exception stop

Name: exceptionStop

Result: {text:String}. Object describing the exception that caused the stop.