bread-n-butter icon indicating copy to clipboard operation
bread-n-butter copied to clipboard

Missing notFollowedBy

Open sveyret opened this issue 5 years ago • 9 comments

As you requested, I open a new issue for the functions which where missing for my project. The most important ones were then, skip and notFollowedBy, anyway I saw that the former where already on the TODO list. I think that lookahead is the negative function of notFollowedBy, so it probably is a good idea to add it too, even if I would name it followedBy.

If you want, I have a little bit of available time and can make PRs to add these functions.

sveyret avatar Aug 12 '20 15:08 sveyret

As you saw, then and skip are easy but I don't like the names... Do you have any suggestions? They could also be functions instead of methods? I've spend a lot of time thinking on this and don't have any names I like. .then is confusing because of promises, I think...

What kind of parser did you write? I've actually never used lookahead or notFollowedBy in my own parsers, but I can see how they would be useful.

wavebeem avatar Aug 12 '20 15:08 wavebeem

I never felt that then was a bad name, even regarding promises. In a way, it's working a little bit like a promise.

But if you want another name, let me suggest: but, reset, take, use, thenTake, thenUse

Anyway, I like the fact that they are Parser methods, I find it easier to write a new parser this way.

In my project, I wrote a NoSQL DML parser. I know there are other solutions, but I used notFollowedBy in 2 situations:

  1. be sure that a number is not followed by a (second) decimal point,
  2. be sure that a type identifier is not followed by a letter (this is to ensure that the detected type is not the written type but only beginning the same way).

sveyret avatar Aug 12 '20 16:08 sveyret

I could also add a bnb.alt (bnb.choice maybe?) or something like Parsimmon.alt for when you have big chains, so it's a bit more readable potentially.

// current
const jsonValue: bnb.Parser<JSONValue> = bnb.lazy(() => {
  return jsonObject
    .or(jsonArray)
    .or(jsonString)
    .or(jsonNumber)
    .or(jsonNull)
    .or(jsonTrue)
    .or(jsonFalse)
    .thru(token);
});

// new way?
const jsonValue2: bnb.Parser<JSONValue> = bnb.lazy(() => {
  return bnb
    .choice(
      jsonObject,
      jsonArray,
      jsonString,
      jsonNumber,
      jsonNull,
      jsonTrue,
      jsonFalse
    )
    .thru(token);
});

EDIT: I added it over here

Can you link me to your parser, if it's open source? I'd like to just see notFollowedBy in your code.

wavebeem avatar Aug 13 '20 00:08 wavebeem

Maybe we could have .followedBy(anotherParser) and .notFollowedBy(anotherParser)? Seems like both get decent use (I searched GitHub for code usage), and don't have any clear alternatives. Plus they're kinda confusing to implement yourself.

wavebeem avatar Aug 13 '20 01:08 wavebeem

Good idea for choice, it's also something I was using a lot, even if or was a good workaround. next is a good name, too. Your PR are great.

Unfortunately, I can't provide a link to my parser as it's not public. But don't hesitate and ask me if you want help to implement followedBy and notFollowedBy

sveyret avatar Aug 13 '20 06:08 sveyret

export function lookahead<B>(parser: Parser<B>) {
  return new Parser(function (context) {
    const a = parser.action(context)
    if (a.type !== ActionResultType.OK) {
      return a
    }
    return merge(a, context.ok(a.value))
  })
}

export function notFollowing<B>(parser: Parser<B>) {
  return new Parser(function (context) {
    const a = parser.action(context)
    if (a.type === ActionResultType.OK) {
      const b = context.fail<null>([ `not '${a.value}'` ])
      return merge(a, b)
    }
    return merge(a, context.ok(null))
  })
}

Let me know if there is a problem with restoring original signatures -- I will fix that.

thephoenixofthevoid avatar Dec 14 '20 10:12 thephoenixofthevoid

lookahead is definitely something we missed from Parsimmon. It is quite useful especially when you want to match invalid content (for skipping syntax errors and keep parsing):

const op = bnb.choice(
  bnb.text('&&'), 
  bnb.text('||'));
const fruit = bnb.choice(
  bnb.text('apple'),
  bnb.text('banana'),
  bnb.match(/[^&\|]+/).lookahead(op.or(bnb.eof))  // catch-all for invalid fruit names
);
const fruitExpr = fruit.sepBy(op);

This can tolerate invalid fruit until another operator or EOF is encountered:

fruitExpr.parse('apple||super duper smelly durian&&banana')
=> ['apple', 'super duper smelly durian', 'banana']

hillin avatar Dec 08 '21 07:12 hillin

I might take a look if someone sends a PR, but I haven't been actively working on this library for a while

wavebeem avatar Dec 09 '21 04:12 wavebeem