GDBMIParser.st
author Jan Vrany <jan.vrany@fit.cvut.cz>
Thu, 25 Apr 2019 16:25:29 +0100
changeset 187 cb419023190f
parent 159 5a364902a0fa
child 235 51f916ee4111
permissions -rw-r--r--
Fix `GDBBreakpoint` description to support new-style breakpoint record ...which includes new field `locations` that contains all locations for a multi-location breakpoint. Previous versions printed more than one breakpoint on `=breakpoint-*` events (which was not valid MI syntax as documented). With this fix, `stx:libgdbs` supports both, the old and new records.

"{ Encoding: utf8 }"

"
jv:libgdbs - GNU Debugger Interface Library
Copyright (C) 2015-now Jan Vrany

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License. 

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
"
"{ Package: 'jv:libgdbs' }"

"{ NameSpace: Smalltalk }"

Object subclass:#GDBMIParser
	instanceVariableNames:'source lookahead token token2CommandMappingBlock recorder'
	classVariableNames:''
	poolDictionaries:'GDBCommandStatus'
	category:'GDB-Private'
!

!GDBMIParser class methodsFor:'documentation'!

copyright
"
jv:libgdbs - GNU Debugger Interface Library
Copyright (C) 2015-now Jan Vrany

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License. 

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
"
!

documentation
"
    A parser that reads GDB MI output records from a stream
    and converts them to objects.

    See [1] for description of GDB MI output syntax.

    [author:]
        Jan Vrany <jan.vrany@fit.cvut.cz>

    [instance variables:]

    [class variables:]

    [see also:]
        [1]: https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Output-Syntax.html#GDB_002fMI-Output-Syntax

"
! !

!GDBMIParser class methodsFor:'instance creation'!

on: aStringOrStream
    ^ self new on: aStringOrStream

    "Created: / 27-05-2014 / 23:50:01 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!GDBMIParser methodsFor:'accessing'!

recorder
    ^ recorder
!

recorder:something
    recorder := something.
!

token2CommandMappingBlock: aBlock
    token2CommandMappingBlock := aBlock.

    "Created: / 19-06-2014 / 21:34:52 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!GDBMIParser methodsFor:'initialization'!

on: aStringOrStream
    source := aStringOrStream readStream

    "Created: / 27-05-2014 / 23:50:24 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!GDBMIParser methodsFor:'parsing'!

parseByte
    | c1 c2 |

    c1 := self next.
    c1 isHexDigit ifFalse:[ 
        self error: 'hex digit expected'.
    ].
    c2 := self next.
    c2 isHexDigit ifFalse:[ 
        self error: 'hex digit expected'.
    ].
    ^ (c1 digitValue bitShift: 4) bitOr:c2 digitValue

    "Created: / 22-06-2018 / 11:27:35 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 30-10-2018 / 20:03:22 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseByteArray
    | bytes |
    self expect: $".
    self skipSeparators.
    bytes := ByteArray new writeStream.
    [ self peek ~~ $" ] whileTrue:[
        bytes nextPut: self parseByte.
        self skipSeparators.
    ].
    self next. "/ eat $"
    ^ bytes contents

    "Created: / 22-06-2018 / 11:24:48 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCString
    self expect: $".
    ^ self parseCStringRest: ''.

    "Created: / 27-05-2014 / 23:51:35 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified (format): / 18-06-2014 / 07:19:09 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCStringEscape: char
    |ascii c |

    char == $" ifTrue:[
	^ $".
    ].

    char == $b ifTrue:[
	^ Character backspace
    ].
    char == $t ifTrue:[
	^ Character tab
    ].
    char == $n ifTrue:[
	^ Character cr
    ].
    char == $r ifTrue:[
	^ Character return
    ].
    char == $f ifTrue:[
	^ Character newPage
    ].

"/    char == $u ifTrue:[
"/        ascii := 0.
"/        c := source peekOrNil.
"/        4 timesRepeat:[
"/            (c isDigitRadix:16) ifFalse:[
"/                self syntaxError:'invalid hex character constant'
"/                        position:source position-2 to:(source position - 1).
"/                ^ Character value:ascii
"/            ].
"/            ascii := (ascii bitShift:4).
"/            ascii := ascii + c digitValue.
"/            source next. c := source peekOrNil.
"/        ].
"/        ^ Character value:ascii
"/    ].
    char == $x ifTrue:[
	ascii := 0.
	c := self peekOrNil.
	2 timesRepeat:[
	    (c isDigitRadix:16) ifFalse:[
		self error:'Invalid hex character escape'.
		^ Character value:ascii
	    ].
	    ascii := (ascii bitShift:4).
	    ascii := ascii + c digitValue.
	    self next. c := self peekOrNil.
	].
	^ Character value:ascii
    ].
    "OctalEscape ::= \ OctalDigit |
		     \ OctalDigit OctalDigit
		     \ ZeroToThree OctalDigit OctalDigit"

    (char between:$0 and:$3) ifTrue:[
	ascii := char digitValue.
	c := self peekOrNil.
	(c between: $0 and: $7) ifTrue:[
	    self next.
	    ascii := (ascii bitShift:3).
	    ascii := ascii + c digitValue.
	].
	c := self peekOrNil.
	(c between: $0 and: $7) ifTrue:[
	    self next.
	    ascii := (ascii bitShift:3).
	    ascii := ascii + c digitValue.
	].
	^ Character value:ascii
    ].
    (char between:$4 and: $7) ifTrue:[
	ascii := char digitValue.
	c := self peekOrNil.
	(c between: $0 and: $7) ifTrue:[
	    self next.
	    ascii := (ascii bitShift:3).
	    ascii := ascii + c digitValue.
	].
	^ Character value:ascii
    ].

    ^ char

    "Created: / 28-05-2014 / 00:02:40 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCStringRest: alreadyRead
    | buffer inString nextChar |

    buffer := (String new: 50) writeStream.
    buffer nextPutAll: alreadyRead.
    nextChar := self next.
    inString := true.
    [inString] whileTrue:[
        nextChar isNil ifTrue:[
            self error: 'Unexpected end of input'.
        ].
        nextChar == $\ ifTrue:[
            nextChar := self next.
            nextChar := self parseCStringEscape:nextChar.
        ] ifFalse:[
            (nextChar == $") ifTrue:[
                (self peekOrNil == $") ifTrue:[
                    self next
                ] ifFalse:[
                    inString := false
                ]
            ].
        ].
        inString ifTrue:[
            buffer nextPut:nextChar.
            nextChar := self next
        ]
    ].
    ^ buffer contents

    "Created: / 18-06-2014 / 07:17:34 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseNl
    | c |

    c := self peek.
    c == Character return"CR" ifTrue:[ 
        self next.
        c := self peek.
    ].
    c == Character lf"LF" ifTrue:[
        self next
    ].

    "Created: / 30-05-2014 / 09:52:36 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 31-05-2014 / 00:38:17 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 12-01-2018 / 08:56:40 / jv"
!

parseNonBlankSequence
    ^ String streamContents:[ :buffer | 
        [ self peek isNil or: [ self peek isSeparator ] ] whileFalse:[
            buffer nextPut: self next.
        ]
    ]

    "Created: / 24-06-2014 / 23:19:40 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseOutput
    "
    output → ( out-of-band-record )* [ result-record ] '(gdb)' nl
    out-of-band-record → async-record | stream-record

    "

    | peek events |

    events := GDBEventSet new.

    peek := self peek.
    peek == Character space ifTrue:[ self next ].
    peek := self peek.
    peek == Character cr ifTrue:[ self next ].
    [
        token := nil.
        peek := self peek.
        peek isNil ifTrue:[ 
            source atEnd ifTrue:[ ^ events ].
        ].
        peek isDigit ifTrue:[ self parseToken. peek := self peek ].
        peek ~~ $(
    ] whileTrue:[
        ('*+=' includes: self peek) ifTrue:[
            | event |

            event := self parseAsyncRecord.
            events add: event.
            "Sigh, GDB 7.7.1 does not write '(gdb)' terminator after an
             async events (even though specification says it should). 
             Thus, report any execution record immediately"
            event isAsyncEvent ifTrue:[
                events last token: token.
                recorder notNil ifTrue:[ 
                    recorder recordResponseEnd.
                ].
                ^ events
            ].
        ] ifFalse:[
            ('~@&' includes: self peek) ifTrue:[
                events add: self parseStreamRecord.
            ] ifFalse:[
                peek == $^ ifTrue:[
                    events add: self parseResultRecord.
                    "/ Sigh, when using MI over PTY (as suggested by Pedro Alves),
                    "/ the PTY is kept open if GDB exits so peek block for ever.
                    "/ So, if ^exit status is reported, exit immediately. 
                    (events last isCommandResultEvent and:[ events last status == #exit ]) ifTrue:[ 
                        events last token: token. 
                        recorder notNil ifTrue:[ 
                            recorder recordResponseEnd.
                        ].
                        ^ events
                    ].
                ] ifFalse:[
                    self error:'Invalid MI record'.
                ].
            ].
        ].
        events last token: token.
    ].
    self expect: '(gdb)'.
    [ self peek == Character space ] whileTrue:[ self next ].
    self parseNl.
    recorder notNil ifTrue:[ 
        recorder recordResponseEnd.
    ].
    ^ events

    "Created: / 30-05-2014 / 09:52:36 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 11-07-2017 / 11:04:21 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parsePostStartHeader
    "
Python Exception <type 'exceptions.ImportError'> No module named gdb: 
./gdb: warning: 
Could not load the Python gdb module from `/usr/local/share/gdb/python'.
Limited Python support is available from the _gdb module.
Suggest passing --data-directory=/path/to/gdb/data-directory.

=thread-group-added,id='i1'
(gdb) 

    "

    | atBeginingOfLine c |

    atBeginingOfLine := true.
    [ 
        c := source peek.
        atBeginingOfLine and:[ c == $= ]
    ] whileFalse:[ 
        c := source next.
        atBeginingOfLine := c == Character lf
    ].

    "Created: / 01-03-2015 / 08:25:42 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseResultClass
    "
    result-class → 'done' | 'running' | 'connected' | 'error' | 'exit'
    "
    | c1 c2 |

    c1 := self peek.
    c1 == $d ifTrue:[
	self expect: 'done'.
	^ CommandStatusDone.
    ].
    c1 == $r ifTrue:[
	self expect: 'running'.
	^ CommandStatusDone.
    ].
    c1 == $c ifTrue:[
	self expect: 'connected'.
	^ CommandStatusConnected.
    ].
    c1 == $e ifTrue:[
	self next.
	c2 := self peek.
	c2 == $x ifTrue:[
	    self expect: 'xit'.
	    ^ CommandStatusExit.
	].
	c2 == $r ifTrue:[
	    self expect: 'rror'.
	    ^ CommandStatusError.
	].
    ].
    ^ self error:'Unsupported result class'

    "Created: / 31-05-2014 / 00:16:31 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 01-06-2014 / 23:21:11 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseResultRecord
    "
    result-record → '^' result-class ( ',' result )* nl
    "

    | command result status descriptor propertyName propertyDescriptor propertyValue |

    self expect: $^.
    result := GDBCommandResult new.
    status := self parseResultClass.
    result status: status.
    descriptor := GDBMAContainer new.
    (token notNil and:[ token2CommandMappingBlock notNil ]) ifTrue:[ 
        command := token2CommandMappingBlock value: token. 
        command notNil ifTrue:[
            result command: command.
            status ~~ CommandStatusError ifTrue:[
                descriptor := command resultDescription.        
            ].
        ].
    ].

    descriptor klass isNil ifTrue:[ 
        "/ Command result is not an object but list of properties...
        [ self peek == $, ] whileTrue:[
            self next. "/ eat $,
            propertyName := self parseVariable.
            propertyDescriptor := descriptor propertyDescriptorAt: propertyName.
            self expect: $=.
            propertyDescriptor isNil ifTrue:[ 
                propertyValue := self parseValue.
            ] ifFalse:[ 
                propertyValue := propertyDescriptor parseUsingGDBMIParser:self. 
            ]. 
            result propertyAt: propertyName put: propertyValue.
         ].             
    ] ifFalse:[ 
        "/ Command result forms an object.
        "/ Create an object and parse property list as its properties.
        | object |

        object := descriptor klass new.
        self peek == $, ifTrue:[
            self next. "/ eat first , following command status...
            self parsePropertiesFor: object describedBy: descriptor.
        ].
        result propertyAt: #value put: object.
    ].
    self parseNl.
    ^ GDBCommandResultEvent new result: result.

    "Created: / 30-05-2014 / 09:52:35 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 02-02-2018 / 09:35:54 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseToken
    token := 0.
    [ self peek isDigit ] whileTrue:[ 
        token := (token * 10) + (self next codePoint - $0 codePoint).
    ].

    "Created: / 28-05-2014 / 00:14:57 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 03-06-2014 / 00:49:16 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!GDBMIParser methodsFor:'parsing-async events'!

parseAsyncRecord
    "
    async-record → exec-async-output | status-async-output | notify-async-output
    "
    self peek == $* ifTrue:[
        self next.
        ^ self parseAsyncRecord: GDBExecutionEvent.
    ].
    self peek == $+ ifTrue:[
        self next.
        ^ self parseAsyncRecord: GDBStatusEvent .
    ].
    self peek == $= ifTrue:[
        self next.
        ^ self parseAsyncRecord: GDBNotificationEvent.
    ].

    ^ self error: 'Invalid async-record'

    "Created: / 30-05-2014 / 09:57:30 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 02-06-2014 / 22:19:35 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseAsyncRecord: eventClassBase
    "
    async-output → async-class ( ',' result )* nl
    "

    | type eventClass event propertyName propertyDescriptor descriptor propertyValue |

    type := self parseString.
    eventClass := eventClassBase eventClassForType: type.
    event := eventClass new.
    event type: type.
    descriptor := eventClass description.
    [ self peek == $, ] whileTrue:[
        self next. "/ eat $,
        propertyName := self parseVariable.
        propertyDescriptor := descriptor propertyDescriptorAt: propertyName.
        self expect: $=.
        propertyDescriptor isNil ifTrue:[ 
            propertyValue := self parseValue.
        ] ifFalse:[ 
            propertyValue := propertyDescriptor parseUsingGDBMIParser:self. 
        ]. 
        event propertyAt: propertyName put: propertyValue.
    ].
    self parseNl.

    ^ event

    "Created: / 01-06-2014 / 23:43:36 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 19-06-2014 / 21:43:35 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!GDBMIParser methodsFor:'parsing-commands'!

parseCommand

    self peek isDigit ifTrue:[ 
        self parseToken
    ].

    ^ self peek == $- ifTrue:[ 
        self parseCommandMI
    ] ifFalse:[ 
        self parseCommandCLI
    ].

    "Created: / 24-06-2014 / 23:08:58 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandCLI
    "raise an error: this method should be implemented (TODO)"

    ^ GDBCLICommand new
        token: token;
        value: self nextLine;
        yourself

    "Created: / 24-06-2014 / 23:10:19 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandMI
    "raise an error: this method should be implemented (TODO)"

    | operation className class args |

    self next. "/ eat $-.
    operation := self parseVariable.
    className := ('GDBMI_' , (operation copyReplaceAll: $- with: $_)) asSymbol.
    class := Smalltalk at: className.
    args := OrderedCollection new.
    [ self peek isNil or:[ self peek == Character cr ] ] whileFalse:[  
        self skipSeparators.
        self peek == $" ifTrue:[ 
            args add: self parseCString
        ] ifFalse:[ 
            args add: self parseNonBlankSequence
        ].
    ].
    self next. "/ eat CR.
    ^ (class arguments: args asArray) token: token.

    "Created: / 24-06-2014 / 23:10:18 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 26-08-2014 / 08:02:03 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!GDBMIParser methodsFor:'parsing-misc'!

parseResult
    "
    result → variable = value
    "

    | name value |

    name := self parseVariable.
    self expect: $=.
    value := self parseValue.
    ^ name -> value

    "Created: / 30-05-2014 / 10:15:55 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 31-05-2014 / 00:39:51 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseString
    ^ String streamContents:[:s|
        [ self peek notNil and:[self peek isLetterOrDigit or:['-_' includes: self peek ] ] ] whileTrue:[
            s nextPut: self next.
        ]
    ].

    "Created: / 30-05-2014 / 10:32:02 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 24-06-2014 / 23:30:54 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseVariable
    "
    variable → string
    "
    ^ self parseString

    "Created: / 30-05-2014 / 10:31:11 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!GDBMIParser methodsFor:'parsing-stream output'!

parseStreamRecord
    "
    stream-record → console-stream-output | target-stream-output | log-stream-output
    "

    self peek == $~ ifTrue:[
        self next.
        ^ self parseStreamRecord: GDBConsoleOutputEvent.
    ].
    self peek == $@ ifTrue:[
        self next.
        ^ self parseStreamRecord: GDBTargetOutputEvent.
    ].
    self peek == $& ifTrue:[
        self next.
        ^ self parseStreamRecord: GDBLogOutputEvent.
    ].
    self error:'Invalid stream record'

    "Created: / 30-05-2014 / 09:57:31 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 02-06-2014 / 22:21:29 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseStreamRecord: eventClass
    | value |

    value := self parseCString.
    self parseNl.
    ^ eventClass new value: value; yourself

    "Created: / 01-06-2014 / 23:41:55 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!GDBMIParser methodsFor:'parsing-utils'!

expect: aStringOrChar

    | c |
    aStringOrChar isCharacter ifTrue:[
	(self atEnd or:[(c := self next) ~= aStringOrChar]) ifTrue:[
	    self error:('Expected ''%1'' got ''%2''.' bindWith: aStringOrChar with: c).
	].
	^self.
    ].
    aStringOrChar isString ifTrue:[
	aStringOrChar do:[:expected|
	    (self atEnd or:[(c := self next) ~= expected]) ifTrue:[
		self error:('Expected ''%1''.' bindWith: aStringOrChar).
	    ].
	].
	^self.
    ].

    self error:'Invalid expected value'.

    "Created: / 19-11-2012 / 20:08:33 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 28-05-2014 / 00:17:06 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

expectLineEnd
    self expect: Character cr.

    "Created: / 19-11-2012 / 20:06:21 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

expectSpace
    self expect: Character space.

    "Created: / 19-11-2012 / 20:06:27 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!GDBMIParser methodsFor:'parsing-values'!

parseConst
    "
    const → c-string
    "

    ^ self parseCString.

    "Created: / 30-05-2014 / 10:36:04 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseList
    "
    list → '[]' | '[' value ( ',' value )* ']' | '[' result ( ',' result )* ']'
    "

    | list |

    self expect: $[.
    list := self parseListBody.
    self expect: $].
    ^ list

    "Created: / 30-05-2014 / 10:36:06 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 17-06-2014 / 22:14:55 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseListBody
    ^self parseListBody: $]

    "Created: / 17-06-2014 / 22:14:06 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 06-07-2017 / 08:25:16 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseListBody: terminator
    | value object |

    object := OrderedCollection new.
    self peek ~~ terminator ifTrue:[
        [   "do..."
            ('"{[' includes: self peek) ifFalse:[ 
                self parseVariable.
                self expect: $=.
            ].
            value := self parseValue.
            object add: value.
            "...while..."
            (self peek == $,) ifTrue:[ self next. true ] ifFalse:[ false ]     
        ] whileTrue.
    ].
    ^ object asArray

    "Created: / 06-07-2017 / 08:24:54 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 30-10-2018 / 14:29:58 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseTuple
    | object |

    self expect: ${.
    object := self parseTupleBody.
    self expect: $}.
    ^ object

    "Created: / 30-05-2014 / 10:36:05 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified (comment): / 14-06-2014 / 02:28:21 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseTupleBody
    | name value dict |

    "/ Sometimes what looks like a tupple is actually
    "/ braced array, such as in : ...,script={"line1","line2"},...
    self peek = $" ifTrue:[ 
        ^ self parseListBody:$}.
    ].
    "/ OK, a real tuple:
    dict := Dictionary new.
    self peek ~~ $} ifTrue:[
        name := self parseVariable.
        self expect: $=.
        value := self parseValue.
        dict at: name put: value.
        [ self peek == $, ] whileTrue:[
            self next.
            name := self parseVariable.
            self expect: $=.
            value := self parseValue.
            dict at: name put: value.
        ].
    ].
    ^ dict

    "Created: / 14-06-2014 / 02:19:56 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 06-07-2017 / 08:25:32 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseValue
    "
    value → const | tuple | list
    "

    self peek == $" ifTrue:[
	^ self parseConst
    ].
    self peek == ${ ifTrue:[
	^ self parseTuple.
    ].
    self peek == $[ ifTrue:[
	^ self parseList
    ].
    self error:'Invalid value'

    "Created: / 30-05-2014 / 10:31:12 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!GDBMIParser methodsFor:'parsing-values-typed'!

parsePropertiesFor: object describedBy: descriptor
    | propertyName propertyValue propertyDescriptor |

    [ 
        "do..."
        propertyName := self parseVariable.
        propertyDescriptor := descriptor propertyDescriptorAt: propertyName.
        self expect: $=.
        propertyDescriptor isNil ifTrue:[ 
            propertyValue := self parseValue.
        ] ifFalse:[ 
            propertyValue := propertyDescriptor parseUsingGDBMIParser:self. 
        ].
        object propertyAt: propertyName put: propertyValue.
        "...while..."
        (self peek == $,) ifTrue:[ self next. true ] ifFalse:[ false ]
    ] whileTrue.
    ^ object

    "Created: / 19-03-2015 / 07:52:28 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 30-10-2018 / 14:30:44 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseValueAsBoolean
    "
    bool → 'y' | 'n' | value
    "

    self peek == $" ifTrue:[
        | c |

        self expect: $".
        c := self next.
        (c ~~ $y and:[ c ~~ $n and:[ c ~~ $1 and:[ c ~~ $0 ]]] ) ifTrue:[
            | s |

            s := self parseCStringRest: c asString.  
            s = 'true' ifTrue:[ ^ true ].
            s = 'false' ifTrue:[ ^ false ].
            ^ s

        ].
        self peek ~~ $" ifTrue:[ 
            | s |

            s := self parseCStringRest: c asString. 
            s = 'true' ifTrue:[ ^ true ].
            s = 'false' ifTrue:[ ^ false ].
            ^ s
        ].
        self next.
        ^ (c == $y) or:[ c == $1 ]
    ].
    ^ self parseValue

    "Created: / 18-06-2014 / 07:33:59 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 12-02-2018 / 22:21:04 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseValueAsByteArray
    ^ self parseByteArray

    "Created: / 22-06-2018 / 11:18:51 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseValueAsInstanceOf: class
    | description |

    class == Boolean ifTrue:[ 
        description := Magritte::MABooleanDescription new.
    ] ifFalse:[
    class == Integer ifTrue:[ 
        description := Magritte::MANumberDescription new
    ] ifFalse:[
    class == String ifTrue:[ 
        description := Magritte::MAStringDescription new
    ] ifFalse:[
        description := class description. 
    ]]].     

    ^ self parseValueAsListOf: class describedBy: description.

    "Created: / 18-06-2014 / 20:28:39 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 23-09-2014 / 23:16:57 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseValueAsInstanceOf: class describedBy: descriptor
    | object |

    self peek ~~ ${ ifTrue:[ ^ self parseValue ].
    self next. "/ eat {
    object := class new.
    self peek ~~ $} ifTrue:[
        self parsePropertiesFor: object describedBy: descriptor
    ].
    self expect: $}.
    ^ object

    "Created: / 18-06-2014 / 20:28:39 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 19-03-2015 / 07:59:02 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseValueAsInteger
    self peek == $" ifTrue:[
        | c v |

        self expect: $".
        c := self next.
        v := 0.
        c isDigit ifTrue:[
            (c == $0 and:[ self peek == $x ]) ifTrue:[ 
                "/ Find 0x prefix, parse hex number (address or alike)
                self next. "/ eat $x
                c := self next.
                [ c isHexDigit ] whileTrue:[ 
                    v := (v << 4) + (c digitValue).
                    c := self next.
                ].
            ] ifFalse:[
                "/ Parse decimal number
                [ c isDigit ] whileTrue:[ 
                    v := (v * 10) + (c digitValue).
                    c := self next.
                ].
            ].
            c == $" ifTrue:[ 
                ^ v
            ] ifFalse:[ 
                ^ self parseCStringRest:v printString , c  
            ].
        ].
        ^ self parseCStringRest: c asString
    ].
    ^ self parseValue

    "Created: / 18-06-2014 / 07:39:22 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 03-07-2018 / 14:50:15 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseValueAsListElementDescribedBy:descriptor
    | tag |
    self peek isAlphaNumeric ifTrue:[ 
        tag := self parseVariable.
        self expect: $=. 
    ].
    ^ descriptor parseUsingGDBMIParser:self taggedAs: tag.

    "Created: / 11-11-2017 / 12:19:08 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 03-07-2018 / 16:22:34 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseValueAsListOf: class
    | description |

    class == Boolean ifTrue:[ 
        description := Magritte::MABooleanDescription new.
    ] ifFalse:[
    class == Integer ifTrue:[ 
        description := Magritte::MANumberDescription new
    ] ifFalse:[
    class == String ifTrue:[ 
        description := Magritte::MAStringDescription new
    ] ifFalse:[
        description := class description. 
    ]]].     

    ^ self parseValueAsListOf: class describedBy: description.

    "Created: / 18-06-2014 / 21:00:02 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 23-09-2014 / 23:17:03 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseValueAsListOf:class describedBy:descriptor
    ^ self parseValueAsListOf:class describedBy:descriptor allowOmmitedBrackets: false

    "Created: / 18-06-2014 / 21:00:08 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 11-11-2017 / 12:14:50 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseValueAsListOf:class describedBy:descriptor allowOmmitedBrackets: allowOmmitedBrackets
    | list bracketsOmmited |

    self peek == $[ ifTrue:[ 
        self next. "/ eat $[
        bracketsOmmited := false.
    ] ifFalse:[
        allowOmmitedBrackets ifFalse:[
            ^ self parseValue 
        ].
        bracketsOmmited := true.
    ].

    list := OrderedCollection new.
    bracketsOmmited ifTrue:[ 
        [ 
            "do..."    
            list add: (self parseValueAsListElementDescribedBy: descriptor).  
            "...while..."
            (self peek == $,) ifTrue:[ self next ].
            self peek == ${
        ] whileTrue.
    ] ifFalse:[
        self peek ~~ $] ifTrue:[ 
            [ 
                "do..."
                list add: (self parseValueAsListElementDescribedBy: descriptor).  
                "...while..."
                (self peek == $,) ifTrue:[ self next. true ] ifFalse:[ false ]
            ] whileTrue.
        ].
    ].

    bracketsOmmited ifFalse:[
        self expect: $]. "/ eats $]
    ].
    ^ list asNilIfEmpty.

    "Created: / 11-11-2017 / 12:14:23 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 30-10-2018 / 14:31:59 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseValueAsString
    ^ self parseCString

    "Created: / 23-09-2014 / 22:22:11 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!GDBMIParser methodsFor:'streaming'!

atEnd
    ^ lookahead isNil and:[ source atEnd ].

    "Created: / 28-05-2014 / 00:17:06 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 14-06-2014 / 02:27:07 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

next
    | next |
    lookahead notNil ifTrue:[
        next := lookahead.
        lookahead := nil.
        ^ next.
    ].
    next := source next.
    recorder notNil ifTrue:[ 
        recorder recordResponseChar: next.
    ].
    ^ next

    "Created: / 23-10-2012 / 10:57:24 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 22-06-2014 / 21:56:02 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

nextLine
    | line |

    line := lookahead notNil 
                ifTrue:[ lookahead asString , (source nextLine ? '') ] 
                ifFalse:[ source nextLine ? ''].
    lookahead := nil.
    recorder notNil ifTrue:[ 
        recorder recordResponse: line.  
    ].
    ^ line

    "Created: / 23-10-2012 / 11:05:47 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 27-02-2015 / 15:05:06 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

peek
    lookahead notNil ifTrue:[
        ^ lookahead
    ].
    lookahead := source next.
    recorder notNil ifTrue:[ 
        lookahead notNil ifTrue:[
            recorder recordResponseChar: lookahead.
        ].
    ].
    ^ lookahead

    "Created: / 28-05-2014 / 00:18:32 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 23-06-2014 / 09:34:33 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

peekOrNil
    lookahead notNil ifTrue:[ ^ lookahead ].
    source atEnd ifTrue:[ ^ nil ].
    ^ self peek

    "Created: / 28-05-2014 / 00:15:32 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

skipSeparators
    [ self peek notNil and:[ self peek isSeparator ] ] whileTrue:[ self next ]

    "Created: / 19-11-2012 / 20:05:04 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 24-06-2014 / 23:31:08 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!GDBMIParser class methodsFor:'documentation'!

version_HG

    ^ '$Changeset: <not expanded> $'
! !