Live data from Hacker News

Just Write the Parser

tiarkrompf.github.io

51–60 of 85 posts

Re: Just Write the Parser

#51

Author here - happy to answer questions, as always. Thanks also for feedback on the format of the article. It's a bit of an experiment on how to present dense information effectively. Some more rationale here: https://tiarkrompf.github.io/notes/?/octopus-notes/

What's the best way to handle unary "-" and other unary operators? I'd like to be able to write expressions like "-2^-(2+2)" or "a cos b + a sin b". For "-2^-(2+2)" note that exponentiation has higher precedence than negation.

Thanks for the other replies, but I was asking the original author for a suggestion using the presented framework, rather than an alternate algorithm or approach from someone else. As presented, the approach didn't seem to handle unary operators.

I probably should have noted that I am already familiar with Pratt parsing, which seems like something that isn't actually brain-dead simple and obvious in the same way (which is why it was worth writing a paper about in the 1970s.)

Hoping for a reply from the original author to recommend a simple approach to add unary operators.

Re: Just Write the Parser

#52
post #39

Earlier quoted context omitted.

What's the best way to handle unary "-" and other unary operators? I'd like to be able to write expressions like "-2^-(2+2)" or "a cos b + a sin b". For "-2^-(2+2)" note that exponentiation has higher precedence than negation.

Take a look at "Parsing expressions by precedence climbing"[1] by Eli Bendersky. See the "Other resources" section for other approaches to this problem. 1: https://eli.thegreenplace.net/2012/08/02/parsing-expressions...

1) Like the original article, this leaves unary operators as an exercise for the reader (though it does note so explicitly and provides a small hint)

2) This looks more like Pratt parsing vs. the approach described in the article

3) I was asking the original author

Re: Just Write the Parser

#53

Earlier quoted context omitted.

What's the best way to handle unary "-" and other unary operators? I'd like to be able to write expressions like "-2^-(2+2)" or "a cos b + a sin b". For "-2^-(2+2)" note that exponentiation has higher precedence than negation.

Take the current + next node i.e: parse_node + parse_peek When parse_node is an operator you know it is the "-2" in "-2^-2(2+2)" and when parse_peek is the operator it is "-(2+2)" in the same. An example of this: - https://github.com/thysultan/Ally/blob/8ba0b4de7ab104ceae54d...

1) This reference is cryptic

2) I was asking the original author for a simple extension to the presented appraoch

Re: Just Write the Parser

#54
post #43
post #37

Earlier quoted context omitted.

I agree with everything you say, except your advice to use a parser combinator library, because most implement PEGs. Hammer is, of course, an exception to this, as long as you call `h_compile` to tell it to use a different backend. Why not use PEGs? In short, they don't actually remove ambiguity from your grammar, but rather hide it in ways that are difficult to reason about. You're still dependent on code as a defin…

In my experience LPeg does a good job of being comprehensible for most reasonably-sized use cases. It can even parse the grammar of Lua itself. I've personally had not that much trouble translating BNF grammars to PEG, though the result is typically longer. It still satisfies the goal of separating parsing logic from data logic, which is a big step. For more info on the issues with PEGs -- and a paper showing how to…

LPeg also has what it calls match-time captures (http://www.inf.puc-rio.br/~roberto/lpeg/#matchtime), which can be used to parse non-context free grammars like common TLV (tag, length, value) formats. For example, I've written a pure LPeg parser for parsing PKIX objects like X.509 certificates. Example edited code snippets with some high-level and low-level bits:

  -- returns DER object pattern that captures inner value
  local function Cobject(identifier, patt)
    local match

    if lpeg.type(patt) then
      match = function (s)
        return lpeg.match(patt * -P(1), s)
      end
    elseif type(patt) == "function" then
      match = patt
    elseif patt == nil then
      match = function (s)
        return s
      end
    else
      error(sformat("expected function, pattern or nil, got %s", type(patt)), 2)
    end

    return Cmt(identifier, function (s, pos)
      local n, pos = assert(unpacklength(s, pos))
      local s1 = s:sub(pos, pos + n - 1)
      pos = pos + n

      return (function (pos, v, ...)
        if v then
          return pos, v, ...
        else
          return false
        end
      end)(pos, match(s1))
    end)
  end

  local BIT_STRING = Cobject(P"\x03", function (s)
    local pad = s:byte(1) -- first octet is number of padding bits
    assert(pad == 0, "BIT STRING not octet aligned") -- we only support DER
    return s:sub(2)
  end)

  local IA5String = Cobject(P"\x16")

  local OID = function (oid)
    if oid then
      local s = packoid(pkix.txt2oid(oid))
      return P(sformat("\x06%s%s", packlength(#s), s)) * Cc(oid)
    else
      return Cobject(P"\x06", function (s)
        return assert(unpackoid(s))
       end)
    end
  end

  local SEQUENCE = function (patt)
    return Cobject(P"\x30", patt)
  end

  local TBSCertificate = SEQUENCE(Ct(
    Cg(Version, "version") *
    Cg(CertificateSerialNumber, "serialNumber") *
    Cg(AlgorithmIdentifier, "signature") *
    Cg(Name, "issuer") *
    Cg(Validity, "validity") *
    Cg(Name, "subject") *
    Cg(SubjectPublicKeyInfo, "subjectPublicKeyInfo") *
    Cg(UniqueIdentifier(1), "issuerUniqueID")^-1 *
    Cg(UniqueIdentifier(2), "subjectUniqueID")^-1 *
    Cg(Extensions, "extensions")^-1 *
    Cg(P(1)^1, "trash")^-1
  ))

  local Signature = BIT_STRING

  local Certificate = SEQUENCE(Ct(
    Cg(TBSCertificate, "tbsCertificate") * 
    Cg(AlgorithmIdentifier, "signatureAlgorithm") * 
    Cg(Signature, "signature")
  ))

Re: Just Write the Parser

#55

There's been a ton of code that I've written that in retrospect, I should never have written. But I've never regretted when I wrote a parser. Maybe because it takes such a large activation energy to get over the hump and actually do it, that I only do it when absolutely necessary. But it always seems easier and more useful than I thought before doing it. Now that this post has prompted that realization, I wonder if i…

I have _absolutely_ regretted writing a parser by hand. Once I replaced it with an ANTLR grammar and a comparatively-trivial bit of glue, my thrift parser became not only easier to refactor but more reliable.

My hand-written recursive descent parser was a perennial source of bugs, where ANTLR has yielded almost none. Some fiddly things I was doing with comments became much easier, if not effortless.

I highly, HIGHLY recommend at least starting with a compiler-generator like ANTLR. The "activation cost" of your project will be much lower, and you may find that you never actually _need_ the level of control you give up.

Re: Just Write the Parser

#56
post #13

> Why simpler is better and why you don't need a parser generator. As far as I can see, this isn't fully answered, unless the claim is strictly limited to the question of need. In my case, I certainly want a parser generator. I'm working on a language, and I did a very early version using a hand-rolled recursive descent parser. Then I realized I wanted a syntax that was human friendly, so I graduated to megaparsec. T…

Right below the line you quote, the author makes three arguments: > • It’s highly instructive, in a way that using a parser generator is not. To quote Feynman: “What I cannot create, I do not understand” > • It’s an important skill: most real-world compilers use hand-written parsers because they provide more control over error handling, significant whitespace, etc. > • It’s not actually difficult! The exercises thems…

I know people often read headlines and then instantly comment, but I really did read most of the post.

What I realized after writing most of my comment was that the author was primarily interested in explaining a recursive descent parser, and the arguments you quoted were simply a hook to get people interested.

That's why I phrased my objection to not _fully_ answering the question.

And it's a good question, so I thought the other side deserved some exploration.

> You should hand-roll a parser first, and then, when you see the limitations of your hand-rolled parser, adopt a parser generator, now with full understanding of what the generator is doing for you (and not doing for you).

Writing your own recursive descent parser only teaches you recursive descent. So, sure, you'll have a clear idea of what a parsec derivative is doing since that's also RD, but it won't help you understand what a LALR parser generated by yacc is doing.

The other problem is for someone to use your parser in another language, they have to port the whole thing and maintain that port as your language changes. Talking about "the first time you write it" and pedagogical uses is entirely fair, but it's only the beginning of the story.

> To be convincing, you'd need to do what the author did, (even though you say the author didn't): you'd need to justify your argument with specific examples.

Nope, never said the author didn't provide examples. To be clear, it's an excellent tutorial on how to write a RD parser.

I'm not prepared to rewrite a bunch of code in two styles, but you're welcome to take a look at the expression parser[1].

In this case, I didn't want to write a whole precedence scanner for expressions, and I wanted precedence to be clear to a reader.

So there's some nuance I didn't capture: in Haskell parsing, writing your own RD parser starts to look like parsec because it's such a natural expression of the problem.

If I was going to write my own parsec, I could still probably use an existing combinator[3] because the parsec model is so generic. That's why I wanted to use that to do a more human readable, and thus more complex, syntax.

But, as I mentioned above, recursive descent kinda sucks. As an example, take the parsing for the left-hand side of an assignment[2]. Sometimes I have 'try' calls, other times I don't. Sometimes the ordering around alternatives (the operator) matters, sometimes it doesn't.

I know in abstract why it works one way or another. But, honestly, most of that is in there because it got tests to pass. My interest is in writing a language, not a parser.

And that's really why I switched from megaparsec to a parser generator. Once I got my grammar to be (reasonably) unambiguous, my source was just the plain, trivially readable BNF rules, and I nuked those stupid tests.

[1]: https://gitlab.com/contravariance/tenet-haskell/-/blob/0640c...

[2]: https://gitlab.com/contravariance/tenet-haskell/-/blob/0640c...

[3]: https://hackage.haskell.org/package/parser-combinators-1.2.1...

Re: Just Write the Parser

#57

Considering that something as simple and limited as JSON has been a source of security vulnerabilities from ambiguities in the spec, thanks but nope. Declarative definitions of a grammar help identify those ambiguities... and if the grammar is defined separately from its interpretation, it opens the door to invisible ambiguities that can become another vector for vulnerabilities. Unless you have provable implementati…

Spec ambiguities of JSON mostly come from the insufficient description of data model and well-formedness and not from the syntax itself. Say, to this day (including RFC 8259) duplicate keys from the JSON object are not explicitly forbidden, even though most applications require that. Probably the only issue arisen from the JSON syntax proper would be the treatment of line and paragraph separators.

Re: Just Write the Parser

#58
post #13

> Why simpler is better and why you don't need a parser generator. As far as I can see, this isn't fully answered, unless the claim is strictly limited to the question of need. In my case, I certainly want a parser generator. I'm working on a language, and I did a very early version using a hand-rolled recursive descent parser. Then I realized I wanted a syntax that was human friendly, so I graduated to megaparsec. T…

How do you handle good error messages or error recovery? I've played around with parser generators but I never figured out how to do either in a satisfactory fashion. Granted, my hand written parser doesn't do error recovery very well either.

I'm not entirely settled on that.

My working approach is if you're writing a compiler, it wants to have an unambiguous grammar and shouldn't even attempt recovery. If input doesn't parse, the compiler can recommend (or just run) the linter. That keeps the common case fast and simple.

The linter/fixer can have a relaxed syntax specifically designed to handle messy code and suggest corrections. That comes from a philosophy of treating error correction and user assistance as a separate task.

But that's an approach I'm taking because I want to get a reference implementation together as quickly as possible. It's definitely not how modern IDEs work.

JetBrains GrammarKit uses a PEG[1] because they need strong support for error recovery[2] using hints. Another interesting library is Tree-sitter[3]; it does incremental parsing keeping an AST constantly up to date for you.

Relevant to this whole discussion, GvR wrote a series on PEG parsers[4] in which he starts out by writing one by hand and then shows how to write one that accepts a grammar.

[1]: https://github.com/JetBrains/Grammar-Kit

[2]: https://github.com/JetBrains/Grammar-Kit#attributes-for-erro...

[3]: https://tree-sitter.github.io/tree-sitter/

[4]: https://medium.com/@gvanrossum_83706/peg-parsing-series-de5d...

Re: Just Write the Parser

#60
post #37

Earlier quoted context omitted.

I agree with everything you say, except your advice to use a parser combinator library, because most implement PEGs. Hammer is, of course, an exception to this, as long as you call `h_compile` to tell it to use a different backend. Why not use PEGs? In short, they don't actually remove ambiguity from your grammar, but rather hide it in ways that are difficult to reason about. You're still dependent on code as a defin…

I firmly disagree about the value of PEGs (and hence combinators) as a formalism that's ambiguous or hard to reason about. PEGs are as well-specified as CFGs, they just work in a different way. One of the strengths of PEGs is that they're never ambiguous. That does mean that order of alternates is important, and sometimes you do have to fiddle with that order when translating something like ABNF. But what you get in…

"PEGs are never ambiguous" is equivalent to solving a problem by proclaiming the wrong solution to be correct.

In that sense, LALR parser generators (e.g. yacc) are also never ambiguous, because even if your grammar is ambiguous in the formal sense, yacc will produce a working parser with well-defined resolution of conflicts...

But the reality is,

    E = E `+` E
is intrinsically ambiguous, no matter how you want to spin it... the only difference is, that LALR generators point that out, whereas PEG generators sweep it under the rug.
Post reply on HN