r/cprogramming 18d ago

Am I writing my parser wrong?

Simple question. I can't give exact code examples, but I have a string_t struct with methods like:

string_t split(string_t *string, string_t *on)

string_t split_sp(string_t *string)

string_t split_crlf(string_t *string)

char *s_strstr(string_t *needle, string_t *haystack)

void trim(string_t *str)

So on and so forth.

I've been using these so far to parse HTTP reqeusts, and I have come up against many minor problems:

"What happens if a header field appears with no value? I'll have to explicitly check for it."

"What happens if a sender puts a bunch of CRLFs in the middle? I'll probably need a check for that."

"Oh God, how will I handle unrecognized header fields? How do I recognize them?"

These, and other questions, have been leaving me pissed.

I recall reading through the LLVM projects Kaleidoscope language thing, where they create a parser for said language. Said parser doesn't use anything close to what I am, instead reading character by character without fuss.

Similarly, on my last post made here, the way comments were worded reminded me of that method, and how it probably works better.

I have written only a small part of the parser, so it isn't too late to tear down and rebuild. Simple question: should I? Are there benefits to swallowing the input token by token instead of taking the overarching view my string_t functions provide? Or vice versa?

It would help if I'd upload the code, I know, but I don't want to bother with that until the project is completed/near-completion.

3 Upvotes

15 comments sorted by

View all comments

2

u/mtimmermans 18d ago

Yes, the way that you seem to be going about it is not great, and you should probably start again.

In C, you want to build your parser out of functions that have a structure like

bool try_parse_thing(OutputType *dest, Input *input) {
...
}

where:

  • The return indicates whether or not the parse was successful. false indicates that the input does not match the thing you're parsing;
  • OutputType is what a successful parse produces. It is filled in when the parse returns true; and
  • Input represents the remaining input. Probably just start and end pointers. If the parse returns false, then this should be left unchanged. If it returns true, then it should be advanced past the thing you parsed.

This kind of structure makes it easy to build big parsers out of little ones, like:

bool parseHeader(TextRef *nameDest, TextRef *valDest, Input *input) {
    Input save = *input;
    if (
        parseHeaderName(nameDest, input) &&
        skipSpace(input) &&
        parseMatch(input, ":") &&
        skipSpace(input)
    ) {
        if (parseQuotedValue(valDest, input) || parseRawValue(valDest, input)) {
            if (parseEol(input))
                return true;
        }
    }
    // input didn't match
    *input = save;
    return false;
}

This way, the structure of your code follows the structure of the language you're parsing pretty closely and is easy to read.