upvote
Pratt parsing is a black box

(vinipsmaker.github.io)

Author here.

I was using Pratt parsing for almost a year in a macro-heavy language that I've been developing before finally sitting down on my chair to write my thoughts on Pratt parsing. Code is closed right now because I don't want LLMs training on my code before 0.1, but I'll open source it again later.

Happy to answer questions.

reply
Tangential - on the 'use [] instead of <>' advice that you referenced in the post, I wish more languages would experiment with making types first class values that just get passed in regular () braces instead.

With erased static types, you can do arbitrary operations on types-as-values at compile time and just compile the static result into the runtime code, so it doesn't even cost anything at runtime to do this. Generic functions just turn into curried functions with the type params as their first param set.

My wip language uses '()' for 'always explicit params' and '{}' for 'can be implicit params', but both types and values can be passed into both (of course inference only makes sense for statically known types/values, so 'value' params in curly braces must be known at compile time and are treated like singleton types as well as their actual value in runtime code that uses them).

reply
My own language doesn't try to unify both worlds (values from "type" world and values elsewhere), but my macros can be configured to happen in the type world. From a macro for the type world, you can run normal code to return whatever type you want. It's not the same thing you're talking about, but it's another interesting thing you can do.
reply
I believe zig does this
reply
in logic programming you can just use normal predicates for arbitrary types also.

my understanding is that this makes certain typing decisions undecidable? I agree though the the special parallel type language is a huge amount of added complexity and source of pain.

reply
> special parallel type language is a huge amount of added complexity and source of pain

This was exactly what initially motivated me to write my own language. I was spending a bunch of time writing complex types in Typescript (for SQL query builder type stuff - essentially data schema -> TS type transformation) as what was essentially just a bunch of regular boolean logic and iteration that would be simple, but because the 'type' world uses a completely different syntax to the 'value' world it gets unreadable really fast.

My language was originally basically "what if Typescripts type system treated types as first class values and let you use standard syntax for interacting with them", but I've been finding various other rabbit holes to fall into along the way.

I'd be curious as to what is undecidable as a consequence of this, that sounds like something that's going to bite me in the ass at some point.

reply
I'd like to see a bit more about how compares to peg or something like ohm/ometa - am I correct assuming you pretty much give up the benefits of a readable grammar as documentation?

Maybe I just need to see something super simple like a calculator with expressions side by side to grasp the trade-off?

https://ohmjs.org/

reply
> am I correct assuming you pretty much give up the benefits of a readable grammar as documentation?

This is more of a question language than a parser language. If you have a language that requires a Turing-complete parser than you gave up on grammar. You can use Pratt to parse languages that have grammars.

The language that I'm implementing has procedural macros (hence the parser is Turing-complete). However it has some "structure" that I can port to a tree-sitter parser. The tree-sitter parser for my language won't be 100% accurate, but it'll capture the most important structures.

reply
> You can use Pratt to parse languages that have grammars

Of course. I meant more that unlike with yacc or ohm - you won't have a grammar as an intrinsic artifact - you would have to maintain it and the Pratt code in sync manually?

From looking a bit more at ohm - it generates peg - and is apparently equivalent to Pratt - so I guess one can have the cake and parse it too?

reply
I've written a few Pratt parsers in my time. You don't need a lexer and you can have more parser states than are implied by 'nud' and 'led'. They can be incredibly extensible. For example, if you can parse the expression "{a:1, b:2}" then you might naturally have an Expr state (nud), a Between state (led) and a DictKey state; that might initially accept just an identifier and a colon before recursing into Expr state, but you could later add dynamic expressions, perhaps via a macro, to support [expr] as a key.
reply
Is this the same algorithm commonly known as "recursive descent" parsing and one of the oldest parsing algorithms? The array index example is just recursive descent and the expression parsing is recursive descent with abstracted operators instead of a parsing function per precedence level.

GCC used to use a type of parser generator but switched to recursive descent because the C language is just a little bit too dynamic to make it work well (EG typedefs). They specifically cited the advantage that because you are writing actual code to parse each production rule, you can break the conventions when needed.

reply
Recursive descent and Pratt are two different but related parsing methods. Pratt is simpler and recursive descent is more explicit, but it's not an "either/or" thing since a parser can use both methods.
reply
deleted
reply
> Traditional parsing techniques register semantic actions to production rules.

Are there any professional slop interpreters who can explain what this means?

reply
This is standard CS terminology - if you take a compiler course it'll be explained in the first couple lectures.

The ELI5 is that "semantic actions" are bits of code that a parser generator runs, and "production rules" are patterns that tell it when to run them.

reply
I would usually think of a 'parser' as simply something that tells you how to produce the input given a set of production rules.

For a simple ambiguous arithmetical grammar, the output of a parser fed "1 + 2 * 3" might be something like "[Mul [Add 1 2] 3]", or "AddExpr(1, MulExpr(2, 3))", or "1 2 3 Mul Add", or "1. Start with 1; 2. Add 2; 3. Multiply by 3", or (more likely in practice) in-memory data structures corresponding to one of those. The form of the representation doesn't matter; it's just an indication of the sequence of steps that gets you from 'nothing' to whatever input string was provided.

But there's not really a concept of 'actions' to go with parsing. Parsing tells you how some string was constructed, or how it might have been constructed. What you do with that information is up to you.

In the example above, the parser's interest in a MulExpr is that it can produce a string containing the token '*'. The fact that mathematically two times three equals six doesn't come up.

reply
I assume that they refer to what you do when you write a Yacc or Bison file or any other input file for a similar parser generator.

A Yacc file contains the grammar of the language to be parsed, but to each grammar rule you can attach a C function that will be invoked by the generated parser when matching that rule.

That C function is the "semantic action" registered for the corresponding grammar production rule.

For instance, by registering appropriate "semantic actions" the generated parser can become a language interpreter, e.g. when matching a multiplication expression a function that does multiplication is invoked, and so on.

If the parser is intended for a compiler, not for an interpreter, the "semantic actions" may generate some intermediate format of the compiled program, like that used by LLVM.

reply
Executing the operation inside the semantic action only works for simple calculators. Otherwise you have the problem that the line of code may run multiple times even though it's only parsed once. Usually you make the semantic actions build an AST - a data value that holds the program fragment you just parsed, e.g. new NumberLiteral(atoi(parsed_text)), new FunctionCall(function_identifier_ast, [arg1_ast, arg2_ast]), new BinaryExpression(left_ast, '+', right_ast)

Older compilers with less memory may have compiled each function as it was parsed, but it's pretty typical these days for the parser to output a whole-file AST that you process separately.

reply
Others here already answered your question. In the blogpost, the text that immediately follows already demonstrates the concept, then it contrasts to what's done in Pratt parsing (semantic actions to to terminals instead of non-terminals). Non-terminals is another way to refer to production rules. This difference is a pretty big deal so it's important to mention. The rest of the section goes on to explain the consequences of this difference. None of this is mentioned in other Pratt tutorials that I read before so I doubt a slop machine would write articles of Pratt parsing even mentioning this trait at all.
reply
The article doesnt read like slop. And this is a very clear sentence to anybody who knows any amount of parser theory.

"Production rules" are rules in grammar descriptions for tools like yacc. "Semantic actions" correspond to pieces of code/logic executed whenever a given rule is fully parsed from input.

reply
The blog reads like organic human text, not mechanical slop.

The terminology is baroque, but pretty standard in parsing circles.

Essentially, different parsing algorithms "run side effects" at different stages of the parsing algorithm application, which can make certain things easy or difficult depending on the timing.

E.g.: If parsing runs side effects before it has 100% decided on what rules have been applied, it may need to "back track" and undo side-effects. This can be very painful to code around, so a common solution is to build (only) a temporary abstract syntax tree (AST) in a pure functional manner, and later come back and process that instead of trying to handle every "do and undo" scenario during initial parsing.

Parsers that only run side-effects when a decision is final allow very efficient compilers that directly "do the thing" instead of building temporary structures.

IIRC, Pascal was compiled this way, in a single pass.

reply
That is not slop btw
reply