Search code examples
error-handlingsmalltalkpharopetitparser

How to correctly handle parsing errors with PetitParser or PetitParser2 in Pharo


I know there has been an old question How can a PetitParser parse rule signal an error?. Lukas Renggli has written it is:

in general this is not good style (mixes syntactic and semantic analysis)

My question is how to do it with more complex example correctly?

Here is my example:

I want to parse the following term:

(0.53,00)
     ^

I want to tell the user the error is where the ^ points. Something like: "please remove the extra comma from your input".

The grammar is:

^ openParenthesis, number, closeParenthesis

The tokens:

openParenthesis
    ^ $( asPParser

number
    ^ wholeNumber plus, dot optional, wholeNumber optional

closeParenthesis
    ^ $) asPParser

wholeNumber 
   ^#digit asPParser plus trim

How would you write a code to detect such (additional comma) kind of error?


Solution

  • You can use the negative lookahead parser. I do not remember the API in Smalltalk, but in Dart (a direct port of the original Smalltalk implementation) you would write:

    In Dart for comparison

    final grammar = char('(') 
        & number 
        & char(',').not('please remove the extra comma from your input') 
        & char(')'); 
    

    Original PetitParser in Smalltalk (as Lukas Renggli suggested in comments section)

    To implement it in the original PetitParser (as suggested in comments):

    Add message instance variable to the class definition:

    PPDelegateParser subclass:#PPNotParser
            instanceVariableNames:'message'
            classVariableNames:''
            poolDictionaries:''
            category:'PetitParser-Parsers'
    

    In the PPNotParser>>parseOn:

    parseOn: aPPContext
            | element memento |
            memento := aPPContext remember.
            element := parser parseOn: aPPContext.
            aPPContext restore: memento.
            ^ element isPetitFailure
                    ifFalse: [ PPFailure message: message context: aPPContext ]
    

    Testing with PetitParser2

    To do just testing in Pharo Smalltalk with PetitParser2 you have to do the following - to implement it as in PetitParser above would be probably harder:

    • Add #message to #PP2NotNode:
        PP2DelegateNode subclass: #PP2NotNode
           instanceVariableNames: 'message'
           classVariableNames: ''
           package: 'PetitParser2-Nodes'
    
    • Create setters and getters in #PP2NotNode
       message: aString
           message := aString
       message
           ^ message
    
    • Edit PP2NotNode>>#parseOn: and make decision based on if the message is empty or not:
    parseOn: aPP2Context
        ^ self message isEmptyOrNil
            ifTrue: [ strategy parseOn: aPP2Context ]
            ifFalse: [ strategy parseOn: aPP2Context message: self message ]
    
    • Next create a new method PP2Not>>parseOn:message:
    parseOn: context message: aMessageString
        | memento retval |
        memento := self remember: context.
        retval := node child parseOn: context.
        self restore: context from: memento.
        ^ retval isPetit2Failure
            ifTrue: [ nil ]
            ifFalse: [ PP2Failure message: aMessageString context: context ]
    
    • You have to enhance PP2CompositeNodeTest>>parse: production:to:end:checkResult:
    parse: aString production: production to: expectedResult end: end checkResult: aBoolean
        | ctx |
        ctx := self context.
        resultContext := self
            parse: aString
            withParser: production
            withContext: ctx.
        result := resultContext value.
        resultContext isPetit2Failure
            ifTrue: [ self unableToParse: resultContext withString: aString ].
        self assert: resultContext position equals: end.
        aBoolean
            ifTrue: [ self assert: expectedResult equals: result ].
        ^ result
    
    • Last but not the least create a method PP2CompositeNodeTest>>unableToParse:withString:
    unableToParse: aResultContext withString: aString
        aResultContext message isEmptyOrNil
            ifTrue: [ self deny: aResultContext isPetit2Failure description: 'Unable to parse ' , aString printString ]
            ifFalse: [ self
                    deny: aResultContext isPetit2Failure
                    description:
                        aResultContext message , ' While parsing: ' , aString printString , '; ' , ' See postion: '
                            , aResultContext position asString ]
    

    This returns your error message if the number is followed by a comma, but otherwise lets the next parser deal with the rest in the sequence.