rewrite the GDB "json-izer" properly

This commit is contained in:
Basile Burg 2018-06-04 15:49:45 +02:00
parent e49acc13a6
commit 60c81b5e34
3 changed files with 711 additions and 2 deletions

View File

@ -522,7 +522,7 @@
<PackageName Value="LCL"/>
</Item8>
</RequiredPackages>
<Units Count="60">
<Units Count="61">
<Unit0>
<Filename Value="coedit.lpr"/>
<IsPartOfProject Value="True"/>
@ -836,6 +836,10 @@
<HasResources Value="True"/>
<ResourceBaseClass Value="Form"/>
</Unit59>
<Unit60>
<Filename Value="..\src\ce_gdbmi2json.pas"/>
<IsPartOfProject Value="True"/>
</Unit60>
</Units>
</ProjectOptions>
<CompilerOptions>

View File

@ -13,7 +13,7 @@ uses
ce_processes, ce_dialogs, ce_dubprojeditor, ce_controls, ce_dfmt,
ce_lcldragdrop, ce_stringrange, ce_dlangmaps, ce_projgroup, ce_projutils,
ce_d2synpresets, ce_dastworx, ce_dbgitf, ce_ddemangle, ce_dubproject,
ce_halstead, ce_diff, ce_profileviewer, ce_semver, ce_term;
ce_halstead, ce_diff, ce_profileviewer, ce_semver, ce_term, ce_gdbmi2json;
{$R *.res}

705
src/ce_gdbmi2json.pas Normal file
View File

@ -0,0 +1,705 @@
unit ce_gdbmi2json;
{$I ce_defines.inc}
interface
uses
Classes, SysUtils, fpjson, fgl;
type
TTokenKind = (
tkInvalid, // on error, easier to leave parsing
tkNl, // #13#10 or #10, i.e te "nl" rule
tkToken, // unquoted text
tkAnd, // &
tkAt, // @
tkTiddle, // ~
tkComma, // ,
tkHat, // ^
tkStar, // *
tkLeftSquare, // [
tkRightSquare,// ]
tkLeftCurly, // {
tkRightCurly, // }
tkString, // quoted string (also used for "c-string" but no checked for now)
tkAss, // =
tkPlus, // +
tkGdb, // (gdb)
tkEOF // additional guard for safe lookups
);
PToken = ^TToken;
TToken = record
private
data: pchar;
length: longInt;
kind: TTokenKind;
public
function text: string;
end;
TTokenList = class(specialize TFPGList<PToken>)
procedure popFront();
procedure clearAndDispose;
destructor destroy; override;
end;
TGdbMiNodeKind = (
gnkLogStreamOutput,
gnkTargetStreamOutput,
gnkConsoleStreamOutput
);
(**
* Tokenizes and parses some GDB-MI output, according to the BNF grammar
* (https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Output-Syntax.html)
* and finally returns a JSON structure that follows grammar rules and names.
*)
function gdbmi2json(const str: string): TJSONObject;
implementation
{$IFDEF DEBUG}
var
r: TJSONObject;
t: TTokenList;
{$ENDIF}
procedure TTokenList.popFront();
begin
dispose(Items[0]);
Delete(0);
end;
procedure TTokenList.clearAndDispose;
begin
while count <> 0 do
popFront();
end;
destructor TTokenList.destroy;
begin
inherited;
clearAndDispose;
end;
//TODO-cGDB: handle escapes in TToken.text()
function TToken.text: string;
begin
result := data[0 .. length];
end;
procedure lex(const str: string; list: TTokenList);
var
i: integer;
s: integer;
procedure addToken(k: TTokenKind);
var
t: PToken;
begin
t := new(PToken);
t^.length:= i-s-1;
if (k <> tkEOF) and (k <> tkInvalid) then
t^.data:= @str[s]
else
t^.data:= nil;
if k = tkString then
begin
t^.data += 1;
t^.length -= 2;
end;
t^.kind:= k;
list.Add(t);
s := i;
end;
begin
s := 1;
i := 1;
while i <= str.length do
begin
case str[i] of
'&':
begin
i += 1;
addToken(TTokenKind.tkAnd);
end;
'@':
begin
i += 1;
addToken(TTokenKind.tkAt);
end;
'+':
begin
i += 1;
addToken(TTokenKind.tkPlus);
end;
'~':
begin
i += 1;
addToken(TTokenKind.tkTiddle);
end;
'^':
begin
i += 1;
addToken(TTokenKind.tkHat);
end;
'=':
begin
i += 1;
addToken(TTokenKind.tkAss);
end;
',':
begin
i += 1;
addToken(TTokenKind.tkComma);
end;
'*':
begin
i += 1;
addToken(TTokenKind.tkStar);
end;
'{':
begin
i += 1;
addToken(TTokenKind.tkLeftCurly);
end;
'}':
begin
i += 1;
addToken(TTokenKind.tkRightCurly);
end;
'[':
begin
i += 1;
addToken(TTokenKind.tkLeftSquare);
end;
']':
begin
i += 1;
addToken(TTokenKind.tkRightSquare);
end;
#10:
begin
i += 1;
addToken(TTokenKind.tkNl);
end;
#13:
begin
i += 1;
if str[i] = #10 then
begin
i += 1;
addToken(TTokenKind.tkNl);
end
else
begin
addToken(TTokenKind.tkInvalid);
end;
end;
'(':
begin
if (i + 4 <= str.length) and (str[i .. i + 4] = '(gdb)') then
begin
i += 5;
addToken(TTokenKind.tkGdb);
end
else
begin
addToken(TTokenKind.tkInvalid);
end;
end;
'"':
begin
while true do
begin
i += 1;
if i > str.length then
begin
addToken(TTokenKind.tkInvalid);
break;
end;
if str[i] = '\' then
begin
i += 1;
if str[i] = '"' then
i+= 1;
end;
if str[i] = '"' then
begin
i += 1;
addToken(TTokenKind.tkString);
break;
end;
end;
end
else
begin
while true do
begin
i += 1;
if i > str.length then
begin
addToken(TTokenKind.tkInvalid);
break;
end;
if str[i] in ['&', '~', '@', '=', '{', '}','[', ']', '*', '"', ',' ,
#10, #13] then
begin
addToken(TTokenKind.tkToken);
break;
end;
end;
end;
end;
end;
addToken(TTokenKind.tkEOF);
end;
(**
* BNF: log-stream-output → "&" c-string nl
*)
function parseLogStreamOutput(tokens: TTokenList): TJSONObject;
var
s: string;
begin
if tokens[0]^.kind <> TTokenKind.tkAnd then
exit(nil);
tokens.popFront();
if tokens[0]^.kind <> TTokenKind.tkToken then
exit(nil);
s := tokens[0]^.text();
tokens.popFront();
if tokens[0]^.kind <> TTokenKind.tkNl then
exit(nil);
tokens.popFront();
result := TJSONObject.Create;
result['kind'] := TJSONIntegerNumber.Create(integer(gnkLogStreamOutput));
result['output'] := TJSONString.Create(s);
end;
(**
* BNF: target-stream-output → "@" c-string nl
*)
function parseTargetStreamOutput(tokens: TTokenList): TJSONObject;
var
s: string;
begin
if tokens[0]^.kind <> TTokenKind.tkAt then
exit(nil);
tokens.popFront();
if tokens[0]^.kind <> TTokenKind.tkToken then
exit(nil);
s := tokens[0]^.text();
tokens.popFront();
if tokens[0]^.kind <> TTokenKind.tkNl then
exit(nil);
tokens.popFront();
result := TJSONObject.Create;
result['kind'] := TJSONIntegerNumber.Create(integer(gnkTargetStreamOutput));
result['output'] := TJSONString.Create(s);
end;
(**
* BNF: console-stream-output → "~" c-string nl
*)
function parseConsoleStreamOutput(tokens: TTokenList): TJSONObject;
var
s: string;
begin
if tokens[0]^.kind <> TTokenKind.tkTiddle then
exit(nil);
tokens.popFront();
if tokens[0]^.kind <> TTokenKind.tkToken then
exit(nil);
s := tokens[0]^.text();
tokens.popFront();
if tokens[0]^.kind <> TTokenKind.tkNl then
exit(nil);
tokens.popFront();
result := TJSONObject.Create;
result['kind'] := TJSONIntegerNumber.Create(integer(gnkConsoleStreamOutput));
result['output'] := TJSONString.Create(s);
end;
function parseListValue(tokens: TTokenList): TJSONArray; forward;
function parseTupleValue(tokens: TTokenList): TJSONObject; forward;
function parseResult(tokens: TTokenList; obj: TJSONObject): boolean; forward;
(**
* BNF: value → const | tuple | list
*)
function parseValue(tokens: TTokenList): TJSONData;
begin
result := nil;
case tokens[0]^.kind of
tkString:
begin
result := TJSONString.Create(tokens[0]^.text());
tokens.popFront();
end;
tkLeftCurly:
begin
result := parseTupleValue(tokens);
end;
tkLeftSquare:
begin
result := parseListValue(tokens);
end;
end;
end;
(**
* BNF: list → "[]" | "[" value ( "," value )* "]" | "[" result ( "," result )* "]"
*)
function parseListValue(tokens: TTokenList): TJsonArray;
var
r: TJSONData;
begin
result := nil;
if tokens[0]^.kind <> TTokenKind.tkLeftSquare then
exit(nil);
tokens.popFront();
result := TJsonArray.Create();
if tokens[0]^.kind = TTokenKind.tkRightSquare then
begin
tokens.popFront();
exit;
end;
r := parseValue(tokens);
if r = nil then
begin
result.Free;
result := nil;
exit;
end;
result.Items[0] := r;
while tokens[0]^.kind = tkComma do
begin
tokens.popFront();
r := parseValue(tokens);
if r = nil then
begin
result.Free;
result := nil;
exit;
end;
result.Items[result.Count] := r;
end;
if tokens[0]^.kind <> tkRightSquare then
begin
result.Free;
result := nil;
end;
tokens.popFront();
end;
(**
* BNF: tuple → "{}" | "{" result ( "," result )* "}"
*)
function parseTupleValue(tokens: TTokenList): TJSONObject;
begin
result := nil;
if tokens[0]^.kind <> TTokenKind.tkLeftCurly then
exit(nil);
tokens.popFront();
result := TJSONObject.Create();
if tokens[0]^.kind = TTokenKind.tkRightCurly then
begin
tokens.popFront();
exit;
end;
if not parseResult(tokens, result) then
begin
result.Free;
result := nil;
exit;
end;
while tokens[0]^.kind = tkComma do
begin
tokens.popFront();
if not parseResult(tokens, result) then
begin
result.Free;
result := nil;
exit;
end;
end;
if tokens[0]^.kind <> tkRightCurly then
begin
result.Free;
result := nil;
end;
tokens.popFront();
end;
(**
* BNF: result → variable "=" value
*)
function parseResult(tokens: TTokenList; obj: TJSONObject): boolean;
var
v: TJSONData;
s: string;
begin
result := false;
if tokens[0]^.kind <> TTokenKind.tkToken then
exit;
s := tokens[0]^.text();
tokens.popFront();
if tokens[0]^.kind <> TTokenKind.tkAss then
exit;
tokens.popFront();
v := parseValue(tokens);
if v = nil then
exit;
obj[s] := v;
result := true;
end;
(**
* BNF: result-record → [ token ] "^" result-class ( "," result )* nl
*)
function parseResultRecord(tokens: TTokenList): TJSONObject;
var
r: TJSONObject;
begin
result := TJSONObject.Create;
if tokens[0]^.kind = TTokenKind.tkToken then
begin
result['token'] := TJSONString.Create(tokens[0]^.text());
tokens.popFront();
end;
if tokens[0]^.kind <> TTokenKind.tkHat then
begin
result.free;
exit(nil);
end;
tokens.popFront();
if tokens[0]^.kind <> TTokenKind.tkToken then
begin
result.free;
exit(nil);
end;
result['result-class'] := TJSONString.Create(tokens[0]^.text());
tokens.popFront();
r := TJSONObject.Create();
result['results'] := r;
while tokens[0]^.kind = TTokenKind.tkComma do
begin
tokens.popFront();
if not parseResult(tokens, r) then
begin
result.free;
result := nil;
exit;
end;
end;
end;
(**
* BNF: async-record → exec-async-output | status-async-output | notify-async-output
*)
function parseAsyncRecord(tokens: TTokenList): TJSonObject;
begin
//TODO-cGDB: parse async records
while tokens[0]^.kind <> tkNl do
tokens.popFront();
result := nil;
end;
(**
* BNF: stream-record → console-stream-output | target-stream-output | log-stream-output
*)
function parseStreamRecord(tokens: TTokenList): TJSonObject;
begin
result := nil;
if not assigned (result) then
result := parseConsoleStreamOutput(tokens);
if not assigned (result) then
result := parseTargetStreamOutput(tokens);
if not assigned (result) then
result := parseLogStreamOutput(tokens);
end;
function asyncRecordBegins(tokens: TTokenList): boolean;
begin
result := ((tokens.Count > 0) and (tokens[0]^.kind in [tkStar, tkAss, tkPlus])) or
((tokens.Count > 1) and (tokens[0]^.kind = tkToken)
and (tokens[1]^.kind in [tkStar, tkAss, tkPlus]));
end;
function streamRecordBegins(tokens: TTokenList): boolean;
begin
result := (tokens.Count > 0) and (tokens[0]^.kind in [tkTiddle, tkAt, tkAnd]);
end;
(**
* BNF: out-of-band-record → async-record | stream-record
*)
function parseOutOfBandRecord(tokens: TTokenList): TJSonObject;
begin
if streamRecordBegins(tokens) then
result := parseStreamRecord(tokens)
else
result := parseAsyncRecord(tokens);
end;
function outOfBandRecordBegins(tokens: TTokenList): boolean;
begin
result := asyncRecordBegins(tokens) or streamRecordBegins(tokens);
end;
(**
* BNF: output → ( out-of-band-record )* [ result-record ] "(gdb)" nl
*)
function parseOutput(tokens: TTokenList): TJSonObject;
var
a: TJSonArray;
begin
result := TJSonObject.Create;
if outOfBandRecordBegins(tokens) then
begin
a := TJSONArray.Create;
result['out-of-band-records'] := a;
while outOfBandRecordBegins(tokens) do
a.Items[a.Count] := parseOutOfBandRecord(tokens);
end;
if tokens[0]^.kind <> tkGdb then
begin
result['result-record'] := parseResultRecord(tokens);
end;
if tokens[0]^.kind <> tkGdb then
begin
result.Free;
result := nil;
end;
tokens.popFront();
if tokens[0]^.kind <> tkNl then
begin
result.Free;
result := nil;
end;
tokens.popFront();
if tokens[0]^.kind <> tkEOF then
begin
result.Free;
result := nil;
end;
end;
function gdbmi2json(const str: string): TJSONObject;
var
tokens: TTokenList;
begin
result := nil;
tokens := TTokenList.Create;
try
lex(str, tokens);
if tokens.Count < 2 then
exit;
result := parseOutput(tokens);
finally
tokens.Free;
end;
end;
{$IFDEF DEBUG}
begin
t := TTokenList.Create;
lex('^done,asm_insns=[{address="0x000107c0",func-name="main"}](gdb)'#10,t);
assert(t[0]^.text = '^');
assert(t[1]^.text = 'done');
assert(t[2]^.text = ',');
assert(t[3]^.text = 'asm_insns');
assert(t[4]^.text = '=');
assert(t[5]^.text = '[');
assert(t[6]^.text = '{');
assert(t[7]^.text = 'address');
assert(t[8]^.text = '=');
assert(t[9]^.text = '0x000107c0');
assert(t[10]^.text = ',');
assert(t[11]^.text = 'func-name');
assert(t[12]^.text = '=');
assert(t[13]^.text = 'main');
assert(t[14]^.text = '}');
assert(t[15]^.text = ']');
assert(t[16]^.text = '(gdb)');
assert(t[17]^.text = #10);
assert(t[18]^.kind = tkEOF);
r := parseOutput(t);
with TMemoryStream.Create do
try
WriteAnsiString(r.AsJSON);
SaveToFile('result-record-array-of-objects.json');
finally
free;
end;
t.Free;
r.Free;
///////////////////////////////////////////////
t := TTokenList.Create;
lex('~abc'#10'@efg'#10'&hij'#10'(gdb)'#10,t);
assert(t[0]^.kind = tkTiddle);
assert(t[1]^.text = 'abc');
assert(t[2]^.kind = tkNl);
assert(t[3]^.kind = tkAt);
assert(t[4]^.text = 'efg');
assert(t[5]^.kind = tkNl);
assert(t[6]^.kind = tkAnd);
assert(t[7]^.text = 'hij');
assert(t[8]^.kind = tkNl);
assert(t[9]^.kind = tkGdb);
assert(t[10]^.kind = tkNl);
assert(t[11]^.kind = tkEOF);
r := parseOutput(t);
with TMemoryStream.Create do
try
WriteAnsiString(r.AsJSON);
SaveToFile('out-of-band-records.json');
finally
free;
end;
t.Free;
r.Free;
{$ENDIF}
end.