Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

parser rewrite #9989

Closed
wants to merge 20 commits into from
Closed

parser rewrite #9989

wants to merge 20 commits into from

Conversation

pennae
Copy link
Contributor

@pennae pennae commented Feb 12, 2024

Motivation

the current parser is not really maintainable, slow, leaks memory all over the place, has very surprising short-range syntax interactions (as demonstrated by its large number of syntactic corner cases), uses a C skeleton and thus prohibits use of C++ features, the list just goes on and on. this exchanges the parser for one that is much faster, much more extensible, and reusable.

we have retrofitted some memory management into the old parser that is mostly (but not completely) correct since this was necessary to allow fuzzing of the old parser. we've run fuzzers over both parser for a couple thousands core hours to make sure that all valid inputs of the old parser are also valid inputs of the new one and produce the same AST, and that the new one doesn't accept anything the old one would reject (excluding bison implementation-defined errors like GLR stack exhaustion). unfortunately we can't share the test dataset because the fuzzers have managed to gobble up bits of private data and we will not be reviewing 50MB of junk to remove all of that again.

depends on #9874

Priorities and Process

Add 👍 to pull requests you find important.

The Nix maintainer team uses a GitHub project board to schedule and track reviews.

@github-actions github-actions bot added new-cli Relating to the "nix" command with-tests Issues related to testing. PRs with tests have some priority repl The Read Eval Print Loop, "nix repl" command and debugger labels Feb 12, 2024
in place of inherited() — not quite useful yet since we don't
distinguish plain and inheritFrom attr kinds so far.
this also has the effect of sorting let bindings lexicographically
rather than by symbol creation order as was previously done, giving a
better canonicalization in the process.
for plain inherits this is really just a stylistic choice, but for
inherit-from it actually fixes an exponential size increase problem
during expr printing (as may happen during assertion failure reporting,
on during duplicate attr detection in the parser)
@tomberek tomberek added the idea approved The given proposal has been discussed and approved by the Nix team. An implementation is welcome. label Feb 13, 2024
@fricklerhandwerk fricklerhandwerk added the significant Novel ideas, large API changes, notable refactorings, issues with RFC potential, etc. label Feb 13, 2024
@nixos-discourse
Copy link

This pull request has been mentioned on NixOS Discourse. There might be relevant details there:

https://discourse.nixos.org/t/2024-02-12-nix-team-meeting-minutes-123/39775/1

@roberth roberth mentioned this pull request Feb 23, 2024
desugaring inherit-from to syntactic duplication of the source expr also
duplicates side effects of the source expr (such as trace calls) and
expensive computations (such as derivationStrict).
it's no longer widely used and has a rather confusing meaning now that
inherit-from is handled very differently.
the parser modifies its inputs, which means that sharing them between
the error context reporting system and the parser itself can confuse the
reporting system. usually this led to early truncation of error context
reports which, while not dangerous, can be quite confusing.
we already normalize attr order to lexicographic, doing the same for
formals makes sense. doubly so because the order of formals would
otherwise depend on the context of the expression, which is not quite as
useful as one might expect.
previously we reported the error at the beginning of the binding
block (for plain inherits) or the beginning of the attr list (for
inherit-from), effectively hiding where exactly the error happened.

this also carries over to runtime positions of attributes in sets as
reported by unsafeGetAttrPos. we're not worried about this changing
observable eval behavior because it *is* marked unsafe, and the new
behavior is much more useful.
the parser treats a plain \r as a newline, error reports do not. this
can lead to interesting divergences if anything makes use of this
feature, with error reports pointing to wrong locations in the input (or
even outside the input altogether).
this needs a string comparison because there seems to be no other way to
get that information out of bison. usually the location info is going to
be correct (pointing at a bad token), but since EOF isn't a token as
such it'll be wrong in that this case.

this hasn't shown up much so far because a single line ending *is* a
token, so any file formatted in the usual manner (ie, ending in a line
ending) would have its EOF position reported correctly.
we now keep not a table of all positions, but a table of all origins and
their sizes. position indices are now direct pointers into the virtual
concatenation of all parsed contents. this slightly reduces memory usage
and time spent in the parser, at the cost of not being able to report
positions if the total input size exceeds 4GiB. this limit is not unique
to nix though, rustc and clang also limit their input to 4GiB (although
at least clang refuses to process inputs that are larger, we will not).

this new 4GiB limit probably will not cause any problems for quite a
while, all of nixpkgs together is less than 100MiB in size and already
needs over 700MiB of memory and multiple seconds just to parse. 4GiB
worth of input will easily take multiple minutes and over 30GiB of
memory without even evaluating anything. if problems *do* arise we can
probably recover the old table-based system by adding some tracking to
Pos::Origin (or increasing the size of PosIdx outright), but for time
being this looks like more complexity than it's worth.

since we now need to read the entire input again to determine the
line/column of a position we'll make unsafeGetAttrPos slightly lazy:
mostly the set it returns is only used to determine the file of origin
of an attribute, not its exact location. the thunks do not add
measurable runtime overhead.

notably this change is necessary to allow changing the parser since
apparently nothing supports nix's very idiosyncratic line ending choice
of "anything goes", making it very hard to calculate line/column
positions in the parser (while byte offsets are very easy).
this doesn't help much yet since the state objects themselves also leak
all memory they are given.
now that destructors are hooked up we want to give the C skeleton a
chance to actually run them. since bison does not call destructors on
values that have been passed to semantic actions even when the action
causes an abort we will also have to do some manual deleting.

partially reverts e8d9de9.
storing a pointer only adds an unnecessary indirection and memory allocation.
almost all places where Exprs are passed as pointers expect the pointers
to be non-null. pass them as references instead to encode this
constraint in types.
with the prepatory work done this mostly means turning plain pointers
into unique_ptrs, with all the associated churn that necessitates.
this gives about 20% performance improvements on pure parsing. obviously
it'll be less on full eval, but depending on how much parsing has to be
done (eg whether nixpkgs haskell modules are included or not) it ranges
anywhere from 4% to 10% in our tests.

this has been tested with thousands of core hours of fuzz testing to
ensure that the ASTs produced by the new parser are exactly the same as
the ones produced by the old parser. error messages will
change (sometimes a lot) and are currently not perfect, but we'd rather
leave that open for improvement than having this work rot forever.
@thufschmitt
Copy link
Member

Discussed during the Nix maintainers meeting on 2024-02-28.
This is a big change, will require some close coordination with the whole maintainer team.
@tomberek offered to act as a proxy to make sure the communication is happening.

@nixos-discourse
Copy link

This pull request has been mentioned on NixOS Discourse. There might be relevant details there:

https://discourse.nixos.org/t/2024-02-28-nix-team-meeting-129/40499/1

Copy link

dpulls bot commented Mar 8, 2024

🎉 All dependencies have been resolved !

@tomberek
Copy link
Contributor

tomberek commented Mar 9, 2024

initial high-level look:

  • builds and runs as expected
  • same evaluation results, nothing looks off
  • nrExprs counts very different
  • ~10% faster for something like a NixOS system
  • similar memory usage
  • not yet clear on how the new structure is more extensible or reusable
  • can ExprConcatString improvements also be made to ExprOpUpdate?

please rebase now that #9874 is merged, would make review easier.

@pennae
Copy link
Contributor Author

pennae commented Mar 11, 2024

unfortunately we do not have the energy to push the parser rewrite forward at this time. that's not to say we don't want to still see it happen (we do!), but for time being it'll have be on hold from our side.

@pennae pennae marked this pull request as draft March 11, 2024 19:16
@pennae pennae closed this by deleting the head repository Apr 17, 2024
@nixos-discourse
Copy link

This pull request has been mentioned on NixOS Discourse. There might be relevant details there:

https://discourse.nixos.org/t/2024-06-05-nix-team-meeting-minutes-150/46583/1

@nyabinary
Copy link

I assume with NixOS/nixpkgs#323845 someone else will have to pick up this right?

@aanderse
Copy link
Member

i think this code was merged to lix

@roberth
Copy link
Member

roberth commented Aug 22, 2024

Nix hasn't stood still in the meanwhile.

~10% faster for something like a NixOS system

This would have to be measured again, since #11145 "improves parsing performance by ~6%, maybe more."

#11072 has also repaid at least a bit of tech debt.

I do like the idea of using a parser library instead, because it could indeed be a cleaner solution, but the impact is not as significant as it would have been around 2.20 or what the merge base of this is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
idea approved The given proposal has been discussed and approved by the Nix team. An implementation is welcome. new-cli Relating to the "nix" command repl The Read Eval Print Loop, "nix repl" command and debugger significant Novel ideas, large API changes, notable refactorings, issues with RFC potential, etc. with-tests Issues related to testing. PRs with tests have some priority
Projects
Status: 🏁 Review
Development

Successfully merging this pull request may close these issues.

8 participants