flutterdartgrammarhl7-fhirpetitparser

Using Flutter's PetiteParser to create FHIRPath


I'd like to ask for some guidance using petitparser (I'm updating this question). There's a json-based grammar called FHIRPath that I'm trying to recreate in dart. I'm new to grammars like this, so it's taken me a little while to understand what I want it to do (or what I think I want it to do). I've managed to get it to parse json paths and general functions, it looks something like this:

class FhirPathGrammar extends GrammarDefinition {
  Parser start() => ref0(value).end();

  Parser value() => (ref0(parens) | ref0(dotString) | ref0(path)).star();

  Parser parens() =>
      (char('(') & ref0(value) & char(')')).map((value) => value);

  Parser dotString() =>
      (anyOf('-_') | letter() | digit() | range(0x80, 0x10FFF))
          .plus()
          .flatten();
          
  Parser path() => (char('.') & ref0(dotString)).map((value) => value);
}

If I run this function:

void main() {
  var pathString = 'Patient.name.exists()';
  var definition = FhirPathGrammar();
  final parser = definition.build();
  print(parser.parse(pathString));
}

This is the result:

[Patient, [., name], [., exists], [(, [], )]]

So far so good. But now if I change my grammar class, and add in an equal parser:

class FhirPathGrammar extends GrammarDefinition {
  Parser start() => ref0(value).end();

  Parser value() =>
      (ref0(parens) | ref0(dotString) | ref0(path) | ref0(equal)).star();

  Parser equal() =>
      (ref0(value) & string(' = ') & ref0(value)).map((value) => value);

  Parser parens() =>
      (char('(') & ref0(value) & char(')')).map((value) => value);

  Parser dotString() =>
      (anyOf('-_') | letter() | digit() | range(0x80, 0x10FFF))
          .plus()
          .flatten();

  Parser path() => (char('.') & ref0(dotString)).map((value) => value);
}

I get an error of:

Unhandled exception:
Stack Overflow
#0      ChoiceParser.parseOn  package:petitparser/…/combinator/choice.dart:71
#1      PossessiveRepeatingParser.parseOn  package:petitparser/…/repeater/possessive.dart:59
#2      FlattenParser.parseOn  package:petitparser/…/action/flatten.dart:31
// Then these 4 lines repeat
#3      ChoiceParser.parseOn  package:petitparser/…/combinator/choice.dart:69
#4      PossessiveRepeatingParser.parseOn  package:petitparser/…/repeater/possessive.dart:67
#5      SequenceParser.parseOn  package:petitparser/…/combinator/sequence.dart:39
#6      MapParser.parseOn  package:petitparser/…/action/map.dart:38
// until it gets here
#9491   ChoiceParser.parseOn  package:petitparser/…/combinator/choice.dart:69
#9492   PossessiveRepeatingParser.parseOn  package:petitparser/…/repeater/possessive.dart:67
#9493   SequenceParser.parseOn  package:petitparser/…/combinator/sequence.dart:39
#9494   PickParser.parseOn  package:petitparser/…/action/pick.dart:26
#9495   CastParser.parseOn  package:petitparser/…/action/cast.dart:17
#9496   Parser.parse  package:petitparser/…/core/parser.dart:51
#9497   main  fhir_path/also_main.dart:7
#9498   _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:283:19)
#9499   _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

As @lukas-renggli pointed out, it seems to be going into an infinite loop. So at least I think that's what is going on. But I don't think I understand how it's matching that's causing the infinite loop.


Solution

  • Hard to tell what is going on without a minimal, reproducible example. However, I suspect that one of the parsers in the choice in the star-operator always succeeds without consuming anything (ref0(empty) looks suspicious). This would cause an infinite loop.

    For example, the following parser shows such behavior:

    epsilon().star().parse('');   // loops forever
    

    Answer for the updated question: Your grammar is left-recursive in the equals production. A similar, slightly simpler example would be:

    // Loops forever: expression is recursively called without consuming anything.
    Parser expression() => (ref0(expression) & char('+') & ref0(expression)) 
        | digit();`
    
    // Fixes the infinite loop by forcing the grammar to consume something 
    // at each step.
    Parser expression() => digit().separatedBy(char('+'));
    

    There are various ways to fix your example, depending on the exact behavior you want. The simplest option is to rewrite the grammar to have the equal as a separator between the other options:

    Parser value() => (ref0(parens) | ref0(dotString) | ref0(path))
        .separatedBy(string(" = "));
    

    I recommend that you look at the Expression Builder. It can simplify building expression parsers and avoid common pitfalls.