Rendered at 17:36:50 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
imoverclocked 8 hours ago [-]
The hardest thing about writing a parser is cognitively accepting what is going to be considered valid input. You can make the best parser that is fast and well specified but invariably someone will (ab)use it in an unexpected way.
Famous examples: despite so many initial good intentions, html tags don’t need to be closed, JSON numbers are too often encoded as strings, YAML can look like what most people expect or it can look progressively more like JSON… and on and on.
simonask 8 hours ago [-]
I think the second-hardest thing is to accept that CS spent decades optimizing parsing algorithms and grammars, and this is still a significant part of CS curricula in many places. But the practical reality is that parsing is almost never a bottleneck.
If what you're parsing is within the capacity of humans to interact with (so in the range of tens of kilobytes), a grammar that requires an O(N^2) parser is totally fine.
bregma 7 hours ago [-]
I don't think it is difficult to accept that fundamentals should be taught.
We spend years learning basic arithmetic like the addition of integers. You could very well argue that there is no need for that either because everyone has a calculator app on their phone. This is how dark ages begin.
sacado2 6 hours ago [-]
It drives me crazy how so many people (of all ages) can't do basic math in their head, at least approximately. You need it constantly and it's so much faster than picking up your phone, starting the app, and typing in the operation. I'm so glad my teachers back in the 80s and 90s told me "you won't always have a calculator in your pocket". They were wrong (I do have one) but they were right (it's not always convenient to rely on a calculator).
haileys 7 hours ago [-]
An O(n^2) parser is not fine for the mere reason that I don't know how one would make such a mess of the job in the first place.
A simple recursive-descent parser is easy to write by hand and runs in linear time.
adev_ 6 hours ago [-]
> A simple recursive-descent parser is easy to write by hand and runs in linear time.
Recursive descenrs parsers are not linear.
They are generally O(n^2) and can even can go exponential with some grammars if written naively.
It can be pretty easy to do adverserival attacks on most naive descent parser and bring it to its knees.
Packrat parser [^1] are linear, but they are by no means "trivial 200 lines" type of parsers.
In the face of backtracking the time depends on the complexity of the grammar, since it's basically a brute force search through all the rules.
mhast 7 hours ago [-]
I think the point was that even if you managed to make a O(n*2) parser it will ve fast enough for human entered problems.
srean 7 hours ago [-]
Not for C++ code generated by whole program optimizing compilers. Your "human entered" is doing the heavy lifting. Now that AI is writing code your assertion might be on shaky ground.
simonask 6 hours ago [-]
C++ can be slow to compile, but as I said, parsing is not the bottleneck. Even for really huge automatically generated C++ files, or old-school concatenated "unity builds", the parsing step is generally tiny compared to everything else.
aw1621107 6 hours ago [-]
> Not for C++ code generated by whole program optimizing compilers.
I'd be quite surprised if an optimizing compiler generated C++ code somewhere in its pipeline!
Oh, that is certainly not what I was expecting at all. I stand corrected!
I do have to wonder though - do you know what proportion of the C++ compiler time is spent parsing your generated C++ code vs. optimizing it?
srean 5 hours ago [-]
Unfortunately no.
Felix is quite old at this point. It's a very interesting language, with many interesting ideas. It did not quite take off though.
aw1621107 5 hours ago [-]
What favorite feature(s) do you miss when working in other languages?
srean 5 hours ago [-]
Coroutines, cooperative threading using fibres, type classes, generics, type deduction, easy interface with C++. The functional style, pattern matching. Flow based programming using 'chips and wires' abstraction.
It has many other interesting capabilities, for example, the ability to change its own grammar, that is rather too much, not for a pleb like me. It has unique (linear and affine) types too. It is really quite a handful.
Go did bring coroutines back into limelight but Felix predates Go by a margin.
Skaller, Felix's author, used Felix as a playground for novel language design ideas, so it was always in a state of flux.
aw1621107 2 hours ago [-]
Huh, that does sound like quite the grab bag of features. Think I'll have to find time to further investigate. Thanks for taking the time to elaborate!
pjmlp 6 hours ago [-]
C++26 reflection?
aw1621107 6 hours ago [-]
Oh, true! That's on me for not being specific enough. I was thinking about the optimization pipeline.
nly 7 hours ago [-]
It's "fine" is you ignore adversarial situations.
If someone is taking malicious stabs at your API then you have a problem
simonask 6 hours ago [-]
Yeah, absolutely. I would be worried about a JSON parser facing the internet that had these kinds of problems, for example. But it's only the very first in a long line of potential vulnerabilities such a system has to consider.
What good is a perfect linear-time constant-space parser if the next thing the system does is to allocate hundreds of megabytes of objects representing some deserialized data structure?
The parser is usually the least interesting part.
mamcx 3 hours ago [-]
And harder than that? Report the error, in a way that make some sense.
This is compounded by the fact that you need the semantics involved, the environment (ie: everything on scope), the source (that means you need to keep carrying big strings around).
And what is efficient means to be destructive, but you need instead the opposite for semantics, error messages, optimizations and the like.
craftkiller 4 hours ago [-]
> JSON numbers are too often encoded as strings
There's a good reason for that, since JSON comes from JavaScript, many JSON parsers treat numbers as double-precision floats. By encoding your number as a string, you ensure that the JSON parser has not modified your number.
That's a very explicit and much debated feature, even self closing tags.
It is also one of the main factors that makes HTML distinct from xml. And a big reason why xhtml was created.
I agree that it makes for a much more complicated interpretation.
microgpt 6 hours ago [-]
[flagged]
f311a 9 hours ago [-]
Unfortunately, simple URL parsing breaks on so many things. There is a reason on why every URL parsing library is at least a few thousand LOCs.
One common way to test it is just to pass ipv6 url: http://[f021:d981:b487:e57d:193e:550e::]/
meindnoch 7 hours ago [-]
Is that so?
RFC 3986 Appendix B [1] "Parsing a URI Reference with a Regular Expression":
The following line is the regular expression for breaking-down a well-formed URI reference into its components.
Hm, but I think he's right. The problem comes when you try to break down the authority portion into host and port; TFA's parser treats the first colon as introducing the port, which is wrong.
>The problem comes when you try to break down the authority portion into host and port
That's a problem orthogonal to URI parsing.
You parse the URI with the RFC 3986 regex, which gives you the components: scheme, authority, path, query, fragment.
You're then free to parse any of the components according to your own bespoke rules, e.g. the query string often follows the key=value&key=value&... pattern.
afiori 4 hours ago [-]
> well-formed URI
A parser that assumes the input to be already valid is usually not enough for most applications
according to that regex this is a valid url
__..__..%%%zz..
meindnoch 32 minutes ago [-]
>according to that regex this is a valid url
>__..__..%%%zz..
Correctly. It is a valid relative URI, whose path is "__..__..%%%zz..".
f311a 4 hours ago [-]
Regexes are pretty slow, though.
meindnoch 31 minutes ago [-]
cc @burntsushi
r3d 7 hours ago [-]
Yeah it's complicated, and that's the thing about parsing anything, the more complicated and unpredictable the input and the harder it is to parse.
Does it need to be human readable, does it need to work across all platforms. Does the it need to be secure. These things change everything. Speed, reliability, security pick one.
Your point about being compliant with the real spec is the difference between a 20 line scannf and and a 1000 line function. Yeah. Ha.
mrkeen 9 hours ago [-]
If you draw a line from 'ad-hoc byte-wrangling nonsense' to 'parser combinators', this can't be more than 20% along it.
Looking at the linked URL parser, why doesn't it look like
url = do scheme
authority
path
query
fragment
where
scheme = ...
authority = ...
etc.
It looks totally ad-hoc.
zabzonk 6 hours ago [-]
FORTH parsers are ultra simple - get the next space-separated token, if it is a number, push it on the stack, otherwise it's a word - look it up in the dictionary and (if it exists there) execute it.
roaringrocky 5 hours ago [-]
Even simpler is you go the colorforth route, part of the source code is "pre parsed" by the editor, a prefix byte is added to each word (which is shown as different colors), then a simple dispatch loop with that prefix as a sort of opcode.
conartist6 3 hours ago [-]
This feels like a precursor to the project I'm doing, which I us to make an editor whose state and output are expressed as such "pre-parsed" syntax trees. I'm trying to make mine fully language agnostic though
Retr0id 9 hours ago [-]
> LineReader splits input into lines, handles \n and \r\n, and trims the stray trailing \r that malformed input likes to leave behind
Is there a common source of extra \r in malformed inputs, beyond those existing as part of \r\n? Or is this just a dig at Windows-style line endings? If there's something weird going on I think I'd rather fail loudly.
> Bounding the inner scanner to a single line makes “run past the end of a malformed line” unrepresentable rather than merely unlikely.
I don't really see what makes it "unrepresentable", and this reads more like "if you used the right scanning logic, you can't have used the wrong scanning logic".
dspillett 6 hours ago [-]
> Is there a common source of extra \r in malformed inputs, beyond those existing as part of \r\n?
Old Macs and some other systems use \r as their EOL, I still sometimes see that with string values in CSV files y code has to deal with (though I don't think I've seen it as an EOL marker in the format itself for a _long_ time).
Sometimes incorrect cleaning steps can leave them behind, such as replacing \r\n with \n but that replacement not being global: it tests fine on strings with zero or one \r\n but subsequent ones will retain their \r. Also code splitting on \n assuming it will always see just that as EOLs will leave trailing \r characters in place. Also, code cleaning EOLs from strings that are supposed to be one-line-only may replace \n (or \n or \r\n, ignoring the possibility of just \r) with a space or a comma and a space, that could be where the \r characters in certain string values I see in files from clients are coming from.
I suspect that off-by-one errors caused by character counting bugs in UTF8/UTF16 handling may cause splitting on EOLs to be a bit off in some cases, though here you will probably be seeing other data corruption at the same time and an errant \r is one of your smaller problems.
inigyou 9 hours ago [-]
Sure, start with \r\n, split on \n, now you have a stray \r at the end of every input.
Retr0id 9 hours ago [-]
But the preceding clause says it handles \r\n. If you're already handling \r\n, what remaining sources of \r are there, that you'd actually want to silently ignore?
inigyou 8 hours ago [-]
Someone else (possibly you) already split on \n.
Retr0id 7 hours ago [-]
Fair point. I think if something is getting mangled like that I'd rather fail loudly, but it depends on the use case I suppose.
HackerThemAll 7 hours ago [-]
\r\n?|\n
handles all EOL sequences without backtracking. Or write a non-regex equivalent of that.
inigyou 6 hours ago [-]
I'm sure every time you split something on newlines you remember to use a regex.
HackerThemAll 4 hours ago [-]
I tend not to, because it's an overkill, but this regex nicely sums what needs to be done.
thesz 8 hours ago [-]
End of line on Classic Mac is \r.
alexjurkiewicz 8 hours ago [-]
> if (!line.accept('[').isEmpty() ) // [section] header.
Is this really ergonomic?
derdi 6 hours ago [-]
It's not, and the article's attempt at putting lipstick on this pig is nonsensical:
> StringView has no operator bool, so a successful match reads as !scanner.accept('=').isEmpty(). Noisier than returning a bool, but the matched text comes back with the answer instead of requiring a second call to go get it.
Clearly there is a second call, it's the call to isEmpty. Plus, the actual text is lost in this particual example (though we know what it was).
An actual ergonomic way of using this would set things up so that the INI parser's inner code could be written something like this:
"if not accept this character, so this is skipping the match, oh wait, if the result of trying to match is empty, no wait again, it was not empty, i.,e we ARE matching...".
aappleby 6 hours ago [-]
If this is "simple", then I don't think the author has tried a good parser combinator library yet.
langbn 6 hours ago [-]
A parser/compiler could obviously be improved with an LLM (AI!) to suggest improvements to invalid input. That is actually a super good use case of LLM/AI.
Having clang/gcc, or any other parser, implement that is of course impossible, they are too conservative and would rather die than to implement modern helpful tools.
derdi 5 hours ago [-]
You're presumably using an LLM to drive the compiler, and your LLM should be just as capable of reading the compiler's error message and fixing the problem. So why double the work?
speedgoose 9 hours ago [-]
I now use nom to write my parsers. Once you understand it, it’s simple and parsing complex data becomes a _fun_ puzzle. I recommend it.
> trims the stray trailing \r that malformed input likes to leave behind
how does this distinguisg the non-stray variety?
jdw64 8 hours ago [-]
I'm going to collect this post after 24 hours, extract the methodologies from everyone's comments, and write them down in my notes. The reason I like HN is that people freely share their tips in the comments
If you created a format that is so difficult to parse that it cannot be parsed with simple readable C code then the problem is the format not the parser code.
jrimbault 8 hours ago [-]
Can you feel the irony when typing this? "Simple readable C code" itself not being able to be parsed by "simple readable C code".
r3d 7 hours ago [-]
Yeah, I agree. But C code parsing is a common and solved problem. The myriad of things people want to store and recover is not though right.
tester756 7 hours ago [-]
Why care about lang which doesnt really support strings well?
mrkeen 6 hours ago [-]
In C's defence, you only have forward-compatability of string handling to the extent that your string type will not change in the future. Byte arrays are great for this. All string encodings in the foreseeable future can be encoded in bytes. You do not need to recompile coreutils once someone invents utf8-plus.
If you wrote your standard lib in whatever was better-than-C at the time, you might have settled for sized strings (who needs strings longer than 65535 bytes?) instead of \0, and you might have had utf32 or some kind of wide char representation.
r3d 7 hours ago [-]
What do you think the libraries you use to parse these things are doing under the hood?
Maybe you don't care? Fair enough.
tester756 5 hours ago [-]
use 10 meters of wrappers around C and its quirks? :P
tomashubelbauer 8 hours ago [-]
This post doesn't touch on something that makes parsers complicated no matter how simple the grammar: good error messages. Parsing a well formed input is the easy part, but not just spitting out a byte index but actually telling the user why their input is not good and what they could do to make it conform is super hard.
The Rust compiler is a common example of a compiler that does a good job here, and I think it is one of only a few.
iainmerrick 7 hours ago [-]
It actually does touch on this:
Built-in line and column tracking. Any movement across a newline updates the line number, including a backwards seek. getLine and getColumn are always available and both are one-based, which makes decent error messages nearly free.
That doesn't sound like much, but having hand-written plenty of recursive descent parsers, it's most of what you need for good error messages. Just being able to pinpoint where the error occurred is usually 80% of the battle; but keeping track of lines and columns in a hand-written parser is a pain.
Sure, for something like Rust, you need vastly more than that, but parsing is a tiny fraction of what the Rust compiler is doing -- type-checking and borrow checking is much more complicated and much more important.
A tiny library like this is a great fit for something like an INI file parser.
estebank 6 hours ago [-]
>> Built-in line and column tracking. Any movement across a newline updates the line number, including a backwards seek. getLine and getColumn are always available and both are one-based, which makes decent error messages nearly free.
> That doesn't sound like much, but having hand-written plenty of recursive descent parsers, it's most of what you need for good error messages.
In my experience having access to the appropriate place where the parser failed is necessary but wholly insufficient for good diagnostics.
whstl 6 hours ago [-]
What I normally do is: only storing the byte position during the parsing itself, and then extracting the line + column when/if displaying an error.
This allows the error report mechanism to be decoupled, and the hot path of the parser has a bit less code to manage.
tgv 7 hours ago [-]
I don't really agree. Many top-down parsers find an error at an unexpected token. That token is often not the error. Quite often something is missing at that point, or there has been a mistake some way back. Translating e.g. "unexpected semicolon" into "keyword 'if' should be the identifier 'f'" is not easy.
spockz 7 hours ago [-]
I think it becomes easier if you have some oracle, like a compiler, available to check whether the end result (after introducing suggestions) is viable.
I say it like this because to me the only valid way to come to the latter class of error messages (containing constructive suggestions) is by first coming up with possible edits and then checking whether they make the whole parse and compile.
spockz 7 hours ago [-]
Doaitse Swierstra’s parser combinators have included this for a while. I seem to recall them also having optional support for self-healing such as adding missing commas, parentheses, etc. I’m sure other parser combinators have this as well by now.
estebank 7 hours ago [-]
I will provide some context from having done a lot of that work.
The Rust grammar is actually quite regular, that's why we have things like the turbofish for type parameters (`binding.method::<Type>()`): it makes the grammar unambiguous (a naïve parser would with a complicated grammar that accepts chained comparisons would have to deal with differentiating between `binding.method < value > ()` and `binding.method<Type>()`). But that doesn't mean the rustc parser doesn't do the work of supporting some the more complex grammar in order to provide better diagnostics. I like to say that rustc actually knows about meta-Rust, a daughter language that goes crazier in its features. I also joke that rustc isn't done until you can paste code from another language and following the suggestions you end up with valid Rust code without loss of the user's intent.
Part of the problem is that the places where incorrect code can fail is in more places than the parser. The chained comparisons example is one that is easy for Rust (as it doesn't support them), so the parser itself can produce a "missing turbofish" suggestion with high certainty, but for truly ambiguous expressions, the errors will happen later, during name resolution ("expected a value and found a type") or when checking the number of arguments. A production compiler needs to account for not only the original error, but also silence every knock-down error too. The simplest strategies are to just stop if at the end of a given stage there are errors (which leads to the "wave of errors" experience of fixing the "last" error leading to a ton of new ones) or fully replacing entire blocks of code that had a parse error with an AST node that acts as a tombstone marking that that later stages need to ignore it. The first option leads to a bad experience, and the latter is insufficient. A recent example of looking at this is https://github.com/rust-lang/rust/pull/159689, where `Arc::new(RwLock::new(HashMap<i32, i64>::default()));` currently produces
error[E0423]: expected value, found struct `HashMap`
--> $DIR/suggest-turbofish-parsed-as-comparisons.rs:11:34
|
LL | let _ = Arc::new(RwLock::new(HashMap<i32, i64>::default()));
| ^^^^^^^
|
--> $SRC_DIR/std/src/collections/hash/map.rs:LL:COL
::: $SRC_DIR/std/src/collections/hash/map.rs:LL:COL
|
= note: `HashMap` defined here
error[E0423]: expected value, found builtin type `i32`
--> $DIR/suggest-turbofish-parsed-as-comparisons.rs:11:42
|
LL | let _ = Arc::new(RwLock::new(HashMap<i32, i64>::default()));
| ^^^ not a value
error[E0423]: expected value, found builtin type `i64`
--> $DIR/suggest-turbofish-parsed-as-comparisons.rs:11:47
|
LL | let _ = Arc::new(RwLock::new(HashMap<i32, i64>::default()));
| ^^^ not a value
error[E0425]: cannot find external crate `default` in the crate root
--> $DIR/suggest-turbofish-parsed-as-comparisons.rs:11:53
|
LL | let _ = Arc::new(RwLock::new(HashMap<i32, i64>::default()));
| ^^^^^^^ not found in the crate root
error[E0061]: this function takes 1 argument but 2 arguments were supplied
--> $DIR/suggest-turbofish-parsed-as-comparisons.rs:11:22
|
LL | let _ = Arc::new(RwLock::new(HashMap<i32, i64>::default()));
| ^^^^^^^^^^^ --------------- unexpected argument #2 of type `bool`
|
note: associated function defined here
--> $SRC_DIR/std/src/sync/poison/rwlock.rs:LL:COL
help: remove the extra argument
|
LL - let _ = Arc::new(RwLock::new(HashMap<i32, i64>::default()));
LL + let _ = Arc::new(RwLock::new(HashMap<i32));
|
This is because the expression is syntactically correct as
RwLock::new( HashMap < i32, i64 > ::default() );
^^^^^^^^^^^^ ------- - ---^ --- - ----------- ^
| | | | | | | |
| | | | | | | a function call to `default` in the crate root
| | | | | | a more than binop
| | | | | a value to be compared
| | | | the separator of the second argument to `RwLock::new()`
| | | a value to be compared
| | a less than binop
| a value to be compared
an associated function call
but after that PR it would only be the following, even though the parser hasn't changed:
error: can't compare two types
--> $DIR/suggest-turbofish-parsed-as-comparisons.rs:24:41
|
LL | let _ = Arc::new(RwLock::new(HashMap<i32, i64>::default()));
| ^ ^ these are parsed as "less than" and "greater than"
|
help: you likely intended to write type `HashMap` with type parameters, but type parameters in expression contexts require the use of the "turbofish" `::<>`
|
LL | let _ = Arc::new(RwLock::new(HashMap::<i32, i64>::default()));
| ++
I think that there's a lot of work needed in the parser itself to produce good diagnostics. There are other strategies, like performing multiple parses at a given point when you've reached a known bad state (you've seen a flag-post that shouldn't be there, but that is a signal for a handful of other known cases), or fully consuming the rest of a block when an unrecoverable parse occurred (we're half-way through parsing function arguments, but failed? consume the rest of the statement or of the parent block, accounting for sub-scopes). The latter can cause the rest of the file to be consumed, but that's an edge-case that in practice is much better than a deluge of irrelevant errors.
Another added complexity is how some easy-to-hit errors occur during lexing, which means the compiler has barely any information about the user's code. Mismatched braces/parens is one of those. rustc tries to provide context by keeping a queue of seen open delimiters to point at, and explicitly checking for their indentation level as a heuristic to detect where the user's intent diverged from the code, but that's overly reliant on the code being sanely formatted (thanks to rustfmt-on-save, that's a good bet for many users). For an example of the things rustc can do even in the lexer, you can look at https://github.com/rust-lang/rust/pull/160592.
Famous examples: despite so many initial good intentions, html tags don’t need to be closed, JSON numbers are too often encoded as strings, YAML can look like what most people expect or it can look progressively more like JSON… and on and on.
If what you're parsing is within the capacity of humans to interact with (so in the range of tens of kilobytes), a grammar that requires an O(N^2) parser is totally fine.
We spend years learning basic arithmetic like the addition of integers. You could very well argue that there is no need for that either because everyone has a calculator app on their phone. This is how dark ages begin.
A simple recursive-descent parser is easy to write by hand and runs in linear time.
Recursive descenrs parsers are not linear.
They are generally O(n^2) and can even can go exponential with some grammars if written naively.
It can be pretty easy to do adverserival attacks on most naive descent parser and bring it to its knees.
Packrat parser [^1] are linear, but they are by no means "trivial 200 lines" type of parsers.
[^1]: https://arxiv.org/abs/cs/0603077
In the face of backtracking the time depends on the complexity of the grammar, since it's basically a brute force search through all the rules.
I'd be quite surprised if an optimizing compiler generated C++ code somewhere in its pipeline!
https://felix-lang.github.io/felix/
Ignore the 'scripting' language claim.
I do have to wonder though - do you know what proportion of the C++ compiler time is spent parsing your generated C++ code vs. optimizing it?
Felix is quite old at this point. It's a very interesting language, with many interesting ideas. It did not quite take off though.
It has many other interesting capabilities, for example, the ability to change its own grammar, that is rather too much, not for a pleb like me. It has unique (linear and affine) types too. It is really quite a handful.
Go did bring coroutines back into limelight but Felix predates Go by a margin.
Skaller, Felix's author, used Felix as a playground for novel language design ideas, so it was always in a state of flux.
If someone is taking malicious stabs at your API then you have a problem
What good is a perfect linear-time constant-space parser if the next thing the system does is to allocate hundreds of megabytes of objects representing some deserialized data structure?
The parser is usually the least interesting part.
This is compounded by the fact that you need the semantics involved, the environment (ie: everything on scope), the source (that means you need to keep carrying big strings around).
And what is efficient means to be destructive, but you need instead the opposite for semantics, error messages, optimizations and the like.
There's a good reason for that, since JSON comes from JavaScript, many JSON parsers treat numbers as double-precision floats. By encoding your number as a string, you ensure that the JSON parser has not modified your number.
https://blog.json-everything.net/posts/numbers-are-numbers-n...
That's a very explicit and much debated feature, even self closing tags. It is also one of the main factors that makes HTML distinct from xml. And a big reason why xhtml was created.
I agree that it makes for a much more complicated interpretation.
One common way to test it is just to pass ipv6 url: http://[f021:d981:b487:e57d:193e:550e::]/
RFC 3986 Appendix B [1] "Parsing a URI Reference with a Regular Expression":
The following line is the regular expression for breaking-down a well-formed URI reference into its components.
Let's test your URI with this regex, shall we? [2]
Seems correct to me.[1] https://datatracker.ietf.org/doc/html/rfc3986#appendix-B
[2] https://regexr.com/8nqop
https://github.com/bkaradzic/bx/blob/0b001f5f36579e8aea07efa...
That's a problem orthogonal to URI parsing.
You parse the URI with the RFC 3986 regex, which gives you the components: scheme, authority, path, query, fragment.
You're then free to parse any of the components according to your own bespoke rules, e.g. the query string often follows the key=value&key=value&... pattern.
A parser that assumes the input to be already valid is usually not enough for most applications
according to that regex this is a valid url
__..__..%%%zz..
Correctly. It is a valid relative URI, whose path is "__..__..%%%zz..".
Does it need to be human readable, does it need to work across all platforms. Does the it need to be secure. These things change everything. Speed, reliability, security pick one.
Your point about being compliant with the real spec is the difference between a 20 line scannf and and a 1000 line function. Yeah. Ha.
Looking at the linked URL parser, why doesn't it look like
It looks totally ad-hoc.Is there a common source of extra \r in malformed inputs, beyond those existing as part of \r\n? Or is this just a dig at Windows-style line endings? If there's something weird going on I think I'd rather fail loudly.
> Bounding the inner scanner to a single line makes “run past the end of a malformed line” unrepresentable rather than merely unlikely.
I don't really see what makes it "unrepresentable", and this reads more like "if you used the right scanning logic, you can't have used the wrong scanning logic".
Old Macs and some other systems use \r as their EOL, I still sometimes see that with string values in CSV files y code has to deal with (though I don't think I've seen it as an EOL marker in the format itself for a _long_ time).
Sometimes incorrect cleaning steps can leave them behind, such as replacing \r\n with \n but that replacement not being global: it tests fine on strings with zero or one \r\n but subsequent ones will retain their \r. Also code splitting on \n assuming it will always see just that as EOLs will leave trailing \r characters in place. Also, code cleaning EOLs from strings that are supposed to be one-line-only may replace \n (or \n or \r\n, ignoring the possibility of just \r) with a space or a comma and a space, that could be where the \r characters in certain string values I see in files from clients are coming from.
I suspect that off-by-one errors caused by character counting bugs in UTF8/UTF16 handling may cause splitting on EOLs to be a bit off in some cases, though here you will probably be seeing other data corruption at the same time and an errant \r is one of your smaller problems.
handles all EOL sequences without backtracking. Or write a non-regex equivalent of that.
Is this really ergonomic?
> StringView has no operator bool, so a successful match reads as !scanner.accept('=').isEmpty(). Noisier than returning a bool, but the matched text comes back with the answer instead of requiring a second call to go get it.
Clearly there is a second call, it's the call to isEmpty. Plus, the actual text is lost in this particual example (though we know what it was).
An actual ergonomic way of using this would set things up so that the INI parser's inner code could be written something like this:
"if not accept this character, so this is skipping the match, oh wait, if the result of trying to match is empty, no wait again, it was not empty, i.,e we ARE matching...".
Having clang/gcc, or any other parser, implement that is of course impossible, they are too conservative and would rather die than to implement modern helpful tools.
https://github.com/rust-bakery/nom
> trims the stray trailing \r that malformed input likes to leave behind
how does this distinguisg the non-stray variety?
No codegen, just function calling.
If you wrote your standard lib in whatever was better-than-C at the time, you might have settled for sized strings (who needs strings longer than 65535 bytes?) instead of \0, and you might have had utf32 or some kind of wide char representation.
Maybe you don't care? Fair enough.
The Rust compiler is a common example of a compiler that does a good job here, and I think it is one of only a few.
Built-in line and column tracking. Any movement across a newline updates the line number, including a backwards seek. getLine and getColumn are always available and both are one-based, which makes decent error messages nearly free.
That doesn't sound like much, but having hand-written plenty of recursive descent parsers, it's most of what you need for good error messages. Just being able to pinpoint where the error occurred is usually 80% of the battle; but keeping track of lines and columns in a hand-written parser is a pain.
Sure, for something like Rust, you need vastly more than that, but parsing is a tiny fraction of what the Rust compiler is doing -- type-checking and borrow checking is much more complicated and much more important.
A tiny library like this is a great fit for something like an INI file parser.
> That doesn't sound like much, but having hand-written plenty of recursive descent parsers, it's most of what you need for good error messages.
In my experience having access to the appropriate place where the parser failed is necessary but wholly insufficient for good diagnostics.
This allows the error report mechanism to be decoupled, and the hot path of the parser has a bit less code to manage.
I say it like this because to me the only valid way to come to the latter class of error messages (containing constructive suggestions) is by first coming up with possible edits and then checking whether they make the whole parse and compile.
The Rust grammar is actually quite regular, that's why we have things like the turbofish for type parameters (`binding.method::<Type>()`): it makes the grammar unambiguous (a naïve parser would with a complicated grammar that accepts chained comparisons would have to deal with differentiating between `binding.method < value > ()` and `binding.method<Type>()`). But that doesn't mean the rustc parser doesn't do the work of supporting some the more complex grammar in order to provide better diagnostics. I like to say that rustc actually knows about meta-Rust, a daughter language that goes crazier in its features. I also joke that rustc isn't done until you can paste code from another language and following the suggestions you end up with valid Rust code without loss of the user's intent.
Part of the problem is that the places where incorrect code can fail is in more places than the parser. The chained comparisons example is one that is easy for Rust (as it doesn't support them), so the parser itself can produce a "missing turbofish" suggestion with high certainty, but for truly ambiguous expressions, the errors will happen later, during name resolution ("expected a value and found a type") or when checking the number of arguments. A production compiler needs to account for not only the original error, but also silence every knock-down error too. The simplest strategies are to just stop if at the end of a given stage there are errors (which leads to the "wave of errors" experience of fixing the "last" error leading to a ton of new ones) or fully replacing entire blocks of code that had a parse error with an AST node that acts as a tombstone marking that that later stages need to ignore it. The first option leads to a bad experience, and the latter is insufficient. A recent example of looking at this is https://github.com/rust-lang/rust/pull/159689, where `Arc::new(RwLock::new(HashMap<i32, i64>::default()));` currently produces
This is because the expression is syntactically correct as but after that PR it would only be the following, even though the parser hasn't changed: I think that there's a lot of work needed in the parser itself to produce good diagnostics. There are other strategies, like performing multiple parses at a given point when you've reached a known bad state (you've seen a flag-post that shouldn't be there, but that is a signal for a handful of other known cases), or fully consuming the rest of a block when an unrecoverable parse occurred (we're half-way through parsing function arguments, but failed? consume the rest of the statement or of the parent block, accounting for sub-scopes). The latter can cause the rest of the file to be consumed, but that's an edge-case that in practice is much better than a deluge of irrelevant errors.Another added complexity is how some easy-to-hit errors occur during lexing, which means the compiler has barely any information about the user's code. Mismatched braces/parens is one of those. rustc tries to provide context by keeping a queue of seen open delimiters to point at, and explicitly checking for their indentation level as a heuristic to detect where the user's intent diverged from the code, but that's overly reliant on the code being sanely formatted (thanks to rustfmt-on-save, that's a good bet for many users). For an example of the things rustc can do even in the lexer, you can look at https://github.com/rust-lang/rust/pull/160592.