about summary refs log tree commit diff
path: root/tvix
AgeCommit message (Collapse)AuthorFilesLines
2022-09-03 r/4612 docs(tvix/eval): add doc comment on `compiler::patch_jump`Vincent Ambo1-0/+6
Change-Id: Ifdd7b99223d239d955ac7eeeae95db97eb742bf0 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6276 Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI
2022-09-03 r/4611 refactor(tvix/eval): get rid of Value::Blackhole variantVincent Ambo2-5/+3
This is no longer needed for anything and the extra clone here is not really more costly than constructing a blackhole value in a different place. Change-Id: I5c63085b1b4418b629ea58a42e3bfe9a9b586d76 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6275 Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI
2022-09-03 r/4610 test(tvix/eval): add a test for float representationVincent Ambo2-0/+3
Change-Id: I4893a37719b9bf08b35963d48e6851a194a08aa7 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6274 Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI
2022-09-03 r/4609 docs(tvix/eval): add a note on how to run Nix testsVincent Ambo1-1/+3
Change-Id: I9cd61ac79ed11b4c6580f31c5af5ebbfd45054b6 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6273 Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI
2022-09-03 r/4608 fix(tvix/eval): address all current clippy lintsVincent Ambo6-13/+10
Change-Id: I758fc4f3b9078de7ca6228a75a4351c3e085c4cf Reviewed-on: https://cl.tvl.fyi/c/depot/+/6272 Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI
2022-09-03 r/4607 fix(tvix/eval): correctly escape `${` in stringsVincent Ambo3-12/+17
Without this escape, it is possible for Nix to produce escaped representations which are not literal Nix values again. This was fixed in upstream Nix in https://github.com/NixOS/nix/pull/4012 (though only for eval, not in the REPL) and the updated test is picked from upstream after that commit. Because we run the C++ Nix tests against our test suite as well, this also bumps our custom Nix 2.3 to a commit that includes the cherry-picked fix from the PR above. Change-Id: I478547ade65f655c606ec46f7143932064192283 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6271 Reviewed-by: grfn <grfn@gws.fyi> Reviewed-by: sterni <sternenseemann@systemli.org> Tested-by: BuildkiteCI
2022-09-03 r/4606 refactor(tvix/eval): move resolve_local to Scope structVincent Ambo1-15/+18
This is a more sensible place for this function to live and makes upvalue resolution easier down the line. Change-Id: I48ee39bdcdb4f96a16a327f7015aff60db5b15fb Reviewed-on: https://cl.tvl.fyi/c/depot/+/6270 Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI
2022-09-03 r/4605 refactor(tvix/eval): introduce Closure struct in Value typeVincent Ambo5-10/+17
This struct will carry the upvalue machinery in addition to the lambda itself. For now, all lambdas are wrapped in closures (though technically analysis of the environment can later remove innermost Closure wrapper, but this optimisation may not be worth it). Change-Id: If2b68549ec1ea4ab838fdc47a2181c694ac937f2 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6269 Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI
2022-09-02 r/4604 chore(tvix/eval): Build with --all-targetsGriffin Smith1-0/+1
Primarily to make sure we build benchmark targets, and avoid breaking them Change-Id: I0c43f4cf99ddfd38e7545ef2d8276ef6b240a1e8 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6285 Tested-by: BuildkiteCI Reviewed-by: tazjin <tazjin@tvl.su>
2022-09-02 r/4603 fix(tvix/eval): Fix build of benchmarksGriffin Smith1-1/+1
Interpret was updated to take an optional path arg in 6fe5e2d75 (feat(tvix/eval): resolve relative path literals, 2022-08-12), but since benchmarks aren't building in CI the resulting breakage of benchmarks was missed. Change-Id: I8a93f1b25ae62e2d032fafc153d91977c6466712 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6284 Reviewed-by: tazjin <tazjin@tvl.su> Tested-by: BuildkiteCI
2022-09-02 r/4602 chore(tvix/eval): move compiler module to a new folderVincent Ambo1-0/+0
Change-Id: I76157f9cf1369cd17506de1b1ded1a4fd06f004a Reviewed-on: https://cl.tvl.fyi/c/depot/+/6268 Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI
2022-09-02 r/4601 refactor(tvix/eval): avoid a use of Value::BlackholeVincent Ambo1-2/+2
The blackhole allocation is not going to be cheaper than cloning this. Change-Id: Id3ad44812decb4392830be06645e67bb0a982b96 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6267 Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI
2022-09-02 r/4600 refactor(tvix/eval): separate out `let inherit ...` logicVincent Ambo1-9/+14
Compilation of `let`-expressions is going to become a lot more complicated due to attempts to avoid thunking when encountering internal references, so this is just being moved out of the way. Change-Id: Iecfa4b13d14532e21c2540e6561b4235ce29736a Reviewed-on: https://cl.tvl.fyi/c/depot/+/6266 Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI
2022-09-02 r/4599 chore(tvix/eval): print slightly more information about warningsVincent Ambo1-1/+2
This is just for dev comfort, it's not going to be useful for the final version. Change-Id: I05fdd590097a61085ed641810655d9ddaf8f3511 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6265 Reviewed-by: sterni <sternenseemann@systemli.org> Tested-by: BuildkiteCI
2022-09-02 r/4598 fix(tvix/eval): consider `let ... inherit ...` in dynamic scopesVincent Ambo3-3/+40
In conditions where no dynamic identifiers exist in a scope, inheriting is usually a no-op - *unless* the identifier is not statically known and the scope has a non-empty `with`-stack. Change-Id: Iff4138d9cd4c56e844bc574203708dacc11c3f73 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6264 Reviewed-by: sterni <sternenseemann@systemli.org> Tested-by: BuildkiteCI
2022-09-02 r/4597 refactor(tvix/eval): add NixAttrs::contains functionVincent Ambo2-1/+13
This avoids copying around the value more than needed. Change-Id: I35949d16dad7fb8f76e0f641eaccf48322144777 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6263 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
2022-09-02 r/4596 feat(tvix/eval): implement builtins.catAttrsVincent Ambo2-1/+18
Change-Id: Idf92ac82438fbfcf7b2f6e058830e4744637d8c6 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6262 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
2022-09-02 r/4595 feat(tvix/eval): implement builtins.typeOfVincent Ambo1-0/+3
Change-Id: Ibc5039b444fadf6f9e5cd9132fcd825a871cee06 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6261 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
2022-09-02 r/4594 feat(tvix/eval): implement type-checking builtinsVincent Ambo1-0/+28
Change-Id: I70d7d837beaaed7e10cdc7577d96130f9e1b6d39 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6260 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
2022-09-02 r/4593 feat(tvix/eval): implement 'throw' and 'abort' builtinsVincent Ambo2-1/+18
These do essentially the same, but return different error variants as upstream Nix considers `throw` to be (sometimes) catchable. Change-Id: I1a9ea84567d46fb37287dbf3f3f67052f9382cca Reviewed-on: https://cl.tvl.fyi/c/depot/+/6259 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
2022-09-02 r/4592 refactor(tvix/eval): implement clearer mechanism for globalsVincent Ambo2-15/+60
The set of things that can leak out of `builtins` into the global scope is statically known (it is what Nix 2.3 leaks there, essentially). This is a mild change over the previous mechanism, where instead at the point where the `builtins` set is constructed we "lift" the globals out of there (if they exist). This way users will still eventually be able to add additional builtins, HOWEVER they will not be able to leak them into the global scope. Note that upstream Nix technically leaks _all_ builtins into the global scope using the `__*` prefix, but we are trying to avoid this in Tvix if it is not required in nixpkgs. Change-Id: Ie9dec2ce33740134f3b2464eba3749f421dd5953 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6258 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
2022-09-02 r/4591 feat(tvix/eval): emit warnings when globals are being shadowedVincent Ambo2-0/+2
Change-Id: I7dae6978c2a4548382d7fa059b20ccdf35d2cf7f Reviewed-on: https://cl.tvl.fyi/c/depot/+/6257 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
2022-09-02 r/4590 feat(tvix/eval): add builtins.isNullVincent Ambo1-0/+5
Change-Id: Iae251d41b4ac6b77df56078a954ec3e33b7f9ccf Reviewed-on: https://cl.tvl.fyi/c/depot/+/6256 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
2022-09-02 r/4589 test(tvix/eval): add a simple test for builtins resolutionVincent Ambo2-0/+7
Change-Id: I91f54778b8a17f3448664c21308de656b4b04b3e Reviewed-on: https://cl.tvl.fyi/c/depot/+/6255 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
2022-09-02 r/4588 feat(tvix/eval): introduce mechanism for defining builtinsVincent Ambo5-6/+48
Adds a new builtins module in which builtins can be constructed. The functions in this module should return a correctly structured value to be passed to the compiler's `globals`. This is wired up all the way to the compiler with an example `toString` builtin, available as a global. Note that this does not yet actually behave like the real toString, which has some differences from `Display`. Change-Id: Ibb5f6fbe6207782fdf2434435567fc1bd80039a5 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6254 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
2022-09-02 r/4587 refactor(tvix/eval): handle scope poisoning & globals dynamicallyVincent Ambo1-61/+125
Previously, the tokens that could poison a scope (`true`, `false`, `null`) had individual fields in the scope to track whether or not they were poisoned. This commit sets up new machinery that instead tracks scope poisoning dynamically using a HashMap, and which makes it possible to introduce additional tokens to the top-level ("global") scope that are directly resolved by the compiler by passing a map of runtime values to be used. With this solution, the compiler now contains all machinery required for wiring up builtins resolution. The set of builtins to be exposed at runtime must, however, be constructed *outside* of the compiler and passed in. Everything is prepared for this, but it is not yet wired up (so the only existing builtins are the ones we already had before). Note that this technically opens up an optimisation potential when compiling selection operations, where the attribute set being selected from is `builtins`. The compiler could directly resolve the builtins and place the right values on the stack. Change-Id: Ia7dad3c2a98703e7ea0c6ace1a722d57cc70a65c Reviewed-on: https://cl.tvl.fyi/c/depot/+/6253 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
2022-09-02 r/4586 docs(tvix/eval): add an overview of all builtins in NixVincent Ambo1-0/+120
Change-Id: Ie187f3317046c6c9e59852d4a128f25ceed99309 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6252 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
2022-09-02 r/4585 feat(tvix/eval): add initial representation of builtinsVincent Ambo3-1/+76
Builtins are represented as a Rust function pointer that accepts a vector of arguments, which represents variable arity builtins. Change-Id: Ibab7e662a646caf1172695d876d2f55e187c03dd Reviewed-on: https://cl.tvl.fyi/c/depot/+/6251 Tested-by: BuildkiteCI Reviewed-by: grfn <grfn@gws.fyi>
2022-09-02 r/4584 feat(tvix/eval): compile function applicationsVincent Ambo3-1/+14
Change-Id: I1b9230601895a1f09ef1a8037201147020b85f36 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6250 Reviewed-by: sterni <sternenseemann@systemli.org> Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI
2022-09-02 r/4583 feat(tvix/eval): implement opcode for function calls in VMVincent Ambo3-8/+30
Nix functions always have a single argument and we do not yet make efforts to optimise this in Tvix for known multi-argument functions being directly applied. For this reason, the call instruction is fairly simple and just calls out to construct a new call frame. Note that the logic for terminating the run loop has moved to the top of the dispatch; this is because the loop run needs to be skipped if the call frame for the current lambda has just been dropped. Change-Id: I259bc07e19c1e55cd0a65207fa8105b23052b967 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6249 Reviewed-by: sterni <sternenseemann@systemli.org> Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI
2022-09-02 r/4582 refactor(tvix/eval): add VM::call helper to set up call framesVincent Ambo1-7/+12
Change-Id: Ia7ff572af90ae379b23bbd0f5215cd13a4dc0ab5 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6248 Reviewed-by: grfn <grfn@gws.fyi> Reviewed-by: sterni <sternenseemann@systemli.org> Tested-by: BuildkiteCI
2022-09-01 r/4581 feat(tvix/eval): compile lambda definitionsVincent Ambo1-1/+40
Compiles lambda definitions of the simple form (i.e. without formals arguments) and emits them as constants like any other value. This does not yet implement actually invoking these functions in the VM. Change-Id: Ie1e0a13220b68c1728be229b875f0992e685c5ef Reviewed-on: https://cl.tvl.fyi/c/depot/+/6247 Tested-by: BuildkiteCI Reviewed-by: grfn <grfn@gws.fyi>
2022-09-01 r/4580 refactor(tvix/eval): introduce LambdaCtx structure to compilerVincent Ambo1-7/+28
This structure carries context about the lambda currently being compiled (which may well be the top-level lambda of an input AST). Using the indirection helpers in the compiler, things like the scope, code and constants of the function being compiled are now taken from the current lambda context instead. Change-Id: If5f864d826c2e72855cee4b728ea1830e9b5ac06 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6246 Tested-by: BuildkiteCI Reviewed-by: grfn <grfn@gws.fyi>
2022-09-01 r/4579 refactor(tvix/eval): add compiler accessor for current scopeVincent Ambo1-38/+42
Change-Id: I7488087d95c1b3fb7f70fc29af0d5b0d0a25a428 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6245 Tested-by: BuildkiteCI Reviewed-by: grfn <grfn@gws.fyi>
2022-09-01 r/4578 refactor(tvix/eval): use call frame for top-level lambdaVincent Ambo1-14/+29
This wires up most of the machinery for executing different call frames inside of the VM and stuffs the top-level lambda which the compiler outputs in there, as well. Change-Id: Ib6201b3e3be1af96a4d195f6eb147f452860ffc3 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6242 Tested-by: BuildkiteCI Reviewed-by: grfn <grfn@gws.fyi>
2022-09-01 r/4577 feat(tvix/eval): add call frame struct to VMVincent Ambo1-0/+8
This is going to carry the data for a function invocation inside of the VM. Change-Id: I86664563a7e35697a64294acd37ffde037fbd32d Reviewed-on: https://cl.tvl.fyi/c/depot/+/6241 Tested-by: BuildkiteCI Reviewed-by: grfn <grfn@gws.fyi>
2022-09-01 r/4576 refactor(tvix/eval): return a lambda from the compilerVincent Ambo5-16/+28
Changes the internal compiler plumbing to not just return a chunk of code, but the same chunk wrapped inside of a lambda value. This is one more step towards compiling runtime lambdas. Change-Id: If0035f8e65a2970c5ae123fc068a2396e1d8fd72 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6240 Tested-by: BuildkiteCI Reviewed-by: grfn <grfn@gws.fyi>
2022-09-01 r/4575 refactor(tvix/eval): add accessor indirection helpers to compilerVincent Ambo1-73/+74
With these indirections in place it becomes easier to change internals of the compiler when introducing functions, which need the compiler to be able to target different code chunks. Change-Id: I4eb11572a93c140b1d059ba0a5af905756745d65 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6239 Tested-by: BuildkiteCI Reviewed-by: grfn <grfn@gws.fyi>
2022-09-01 r/4574 feat(tvix/eval): introduce initial `Lambda` typeVincent Ambo2-0/+18
Change-Id: Ifa9766f5ffeff99e926936bafd697e885e733b78 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6238 Tested-by: BuildkiteCI Reviewed-by: grfn <grfn@gws.fyi>
2022-09-01 r/4573 feat(tvix/eval): emit warnings for unused local bindingsVincent Ambo2-20/+50
Change-Id: I6e876a8f4d062297abae812b14ed8ec17a502f2c Reviewed-on: https://cl.tvl.fyi/c/depot/+/6237 Tested-by: BuildkiteCI Reviewed-by: grfn <grfn@gws.fyi>
2022-09-01 r/4572 refactor(tvix/eval): collect vector of errors in compilerVincent Ambo3-102/+111
Instead of exiting the compiler at the first sight of an error, skip any erroneous nodes and continue compiling, collecting more errors along the way. This paves the way for nicer error reporting in which multiple errors can be reported at once, avoiding situations in which users are hunting a fault error-by-error and possibly getting distracted by less useful output. Change-Id: I80c9a87272e33a31297167ae2eb2706a46adf15a Reviewed-on: https://cl.tvl.fyi/c/depot/+/6236 Tested-by: BuildkiteCI Reviewed-by: grfn <grfn@gws.fyi>
2022-09-01 r/4571 feat(tvix/eval): carry optional SyntaxNode in error typeVincent Ambo6-40/+62
This starts paving the way for nicer, source-code based error reporting. Right now the code paths in the VM do not emit annotated errors, as we do not yet preserve that structure from the compiler. However, error emitting code paths in the compiler have been amended to include known nodes. Change-Id: I1b74410ffd891c40cd913361bd73c4336ec8aa5b Reviewed-on: https://cl.tvl.fyi/c/depot/+/6235 Tested-by: BuildkiteCI Reviewed-by: grfn <grfn@gws.fyi>
2022-09-01 r/4570 refactor(tvix/eval): add helper for emitting compiler warningsVincent Ambo1-8/+6
Change-Id: I2d98dbb7274d07985f64e7cc8944e316bf42e1bf Reviewed-on: https://cl.tvl.fyi/c/depot/+/6234 Reviewed-by: sterni <sternenseemann@systemli.org> Tested-by: BuildkiteCI
2022-09-01 r/4569 chore(tvix/eval): bump rnix-parser to latest masterVincent Ambo3-8/+8
In this commit, the string interpolation parsing is identical to nixpkgs which makes some of the upstream Nix tests for interpolation-related weirdness pass. Change-Id: I3a295cfdc404c32228a54846e6efd3c0dcee5842 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6233 Reviewed-by: sterni <sternenseemann@systemli.org> Tested-by: BuildkiteCI
2022-09-01 r/4568 fix(tvix/eval): fix several string escapingsVincent Ambo1-2/+4
These were missing an additional level of escaping, silly oversight caught by an upstream test. Change-Id: I0312084475e4b88c83945614e9aa5b34c6bc3ec2 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6232 Reviewed-by: sterni <sternenseemann@systemli.org> Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI
2022-09-01 r/4567 refactor(tvix/eval): Upgrade to latest rnix-parserVincent Ambo6-490/+382
Since the latest published version of rnix-parser on crates.io, the crate has undergone major changes which are only available in the git repository at the moment. This commit updates the compiler to this newer version of rnix. Most notably, the entire AST provided by rnix is now wrapped in the AST type system. As a result of this traversal is much nicer in many places, especially for things like nested attribute selection. There are a handful of smaller features missing for full feature parity with the previous version, especially handling of path literals, but PRs for these already exist in rnix-parser. Change-Id: Icde6d393067976549492b7d89c4cc49e5e575fc7 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6231 Reviewed-by: sterni <sternenseemann@systemli.org> Tested-by: BuildkiteCI
2022-09-01 r/4566 feat(tvix/eval): implement `assert` operatorVincent Ambo4-0/+27
This implements `assert`, which evaluates an expression and aborts evaluation if the value is not `true`. At this point we should introduce eval-failed-* tests; probably asserting against some representation of the error enum? Change-Id: If54c8f616d89b829c1860a4835dde60a2cd70d7a Reviewed-on: https://cl.tvl.fyi/c/depot/+/6230 Reviewed-by: grfn <grfn@gws.fyi> Reviewed-by: sterni <sternenseemann@systemli.org> Tested-by: BuildkiteCI
2022-09-01 r/4563 refactor(tvix/eval): use pretty_assertions for testsVincent Ambo3-4/+50
This makes for much more readable output especially when long strings are involved. Change-Id: I43dd73a0480535d7181a760788c42883a9b083f8 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6229 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
2022-09-01 r/4562 refactor(tvix/eval): improve naming for locals manipulator methodsVincent Ambo1-5/+9
`push_local`/`push_phantom` were worse names because they sound like the value itself is being pushed, where in actuality it is just being declared to the compiler. Change-Id: Ibfda5c4c8e47d5d3262bfe005b0f1f84908a117e Reviewed-on: https://cl.tvl.fyi/c/depot/+/6228 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
2022-08-31 r/4561 feat(tvix/eval): implement scope poisoning for true/false/nullVincent Ambo5-5/+60
These tokens are optionally parsed as identifiers by Nix, which means that within any scopes that resolve them the compiler needs to track whether they have been overridden to know whether to emit the literal instructions or resolve a variable. This is implemented by a new concept of "scope poisoning", where the compiler's scope structure tracks whether or not any builtin identifiers have been overridden. Change-Id: I3ab711146e229f843f6e1f0343385382ee0aecb6 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6227 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org> Reviewed-by: grfn <grfn@gws.fyi>