diff options
author | John Ericson <John.Ericson@Obsidian.Systems> | 2023-05-13 13:52:45 -0400 |
---|---|---|
committer | John Ericson <John.Ericson@Obsidian.Systems> | 2023-06-27 09:37:54 -0400 |
commit | 22b278e011ab9c1328749a126514c57b89a39173 (patch) | |
tree | 0920c88b6bc996502f5ebf153fcb4c8956321e6e /src | |
parent | d40f0e534d657e619634ba204fb8970f4a01fbc5 (diff) |
Automatically document builtin constants
This is done in roughly the same way builtin functions are documented.
Also auto-link experimental features for primops, subsuming PR #8371.
Co-authored-by: Eelco Dolstra <edolstra@gmail.com>
Co-authored-by: Robert Hensing <roberth@users.noreply.github.com>
Co-authored-by: Valentin Gagarin <valentin.gagarin@tweag.io>
Diffstat (limited to 'src')
-rw-r--r-- | src/libexpr/eval.cc | 37 | ||||
-rw-r--r-- | src/libexpr/eval.hh | 87 | ||||
-rw-r--r-- | src/libexpr/flake/flake.cc | 3 | ||||
-rw-r--r-- | src/libexpr/primops.cc | 279 | ||||
-rw-r--r-- | src/libexpr/primops.hh | 14 | ||||
-rw-r--r-- | src/libexpr/primops/fetchClosure.cc | 3 | ||||
-rw-r--r-- | src/libexpr/value.hh | 10 | ||||
-rw-r--r-- | src/nix/main.cc | 41 |
8 files changed, 386 insertions, 88 deletions
diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 8a0ff4cce..aaeb419ad 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -703,28 +703,34 @@ Path EvalState::toRealPath(const Path & path, const NixStringContext & context) } -Value * EvalState::addConstant(const std::string & name, Value & v) +Value * EvalState::addConstant(const std::string & name, Value & v, Constant info) { Value * v2 = allocValue(); *v2 = v; - addConstant(name, v2); + addConstant(name, v2, info); return v2; } -void EvalState::addConstant(const std::string & name, Value * v) +void EvalState::addConstant(const std::string & name, Value * v, Constant info) { - staticBaseEnv->vars.emplace_back(symbols.create(name), baseEnvDispl); - baseEnv.values[baseEnvDispl++] = v; auto name2 = name.substr(0, 2) == "__" ? name.substr(2) : name; - baseEnv.values[0]->attrs->push_back(Attr(symbols.create(name2), v)); -} + constantInfos.push_back({name2, info}); -Value * EvalState::addPrimOp(const std::string & name, - size_t arity, PrimOpFun primOp) -{ - return addPrimOp(PrimOp { .fun = primOp, .arity = arity, .name = name }); + if (!(evalSettings.pureEval && info.impureOnly)) { + /* Check the type, if possible. + + We might know the type of a thunk in advance, so be allowed + to just write it down in that case. */ + if (auto gotType = v->type(true); gotType != nThunk) + assert(info.type == gotType); + + /* Install value the base environment. */ + staticBaseEnv->vars.emplace_back(symbols.create(name), baseEnvDispl); + baseEnv.values[baseEnvDispl++] = v; + baseEnv.values[0]->attrs->push_back(Attr(symbols.create(name2), v)); + } } @@ -738,7 +744,10 @@ Value * EvalState::addPrimOp(PrimOp && primOp) vPrimOp->mkPrimOp(new PrimOp(primOp)); Value v; v.mkApp(vPrimOp, vPrimOp); - return addConstant(primOp.name, v); + return addConstant(primOp.name, v, { + .type = nThunk, // FIXME + .doc = primOp.doc, + }); } auto envName = symbols.create(primOp.name); @@ -764,13 +773,13 @@ std::optional<EvalState::Doc> EvalState::getDoc(Value & v) { if (v.isPrimOp()) { auto v2 = &v; - if (v2->primOp->doc) + if (auto * doc = v2->primOp->doc) return Doc { .pos = {}, .name = v2->primOp->name, .arity = v2->primOp->arity, .args = v2->primOp->args, - .doc = v2->primOp->doc, + .doc = doc, }; } return {}; diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index 0c07ae081..e3676c1b7 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -25,15 +25,72 @@ struct DerivedPath; enum RepairFlag : bool; +/** + * Function that implements a primop. + */ typedef void (* PrimOpFun) (EvalState & state, const PosIdx pos, Value * * args, Value & v); +/** + * Info about a primitive operation, and its implementation + */ struct PrimOp { - PrimOpFun fun; - size_t arity; + /** + * Name of the primop. `__` prefix is treated specially. + */ std::string name; + + /** + * Names of the parameters of a primop, for primops that take a + * fixed number of arguments to be substituted for these parameters. + */ std::vector<std::string> args; + + /** + * Aritiy of the primop. + * + * If `args` is not empty, this field will be computed from that + * field instead, so it doesn't need to be manually set. + */ + size_t arity = 0; + + /** + * Optional free-form documentation about the primop. + */ const char * doc = nullptr; + + /** + * Implementation of the primop. + */ + PrimOpFun fun; + + /** + * Optional experimental for this to be gated on. + */ + std::optional<ExperimentalFeature> experimentalFeature; +}; + +/** + * Info about a constant + */ +struct Constant +{ + /** + * Optional type of the constant (known since it is a fixed value). + * + * @todo we should use an enum for this. + */ + ValueType type = nThunk; + + /** + * Optional free-form documentation about the constant. + */ + const char * doc = nullptr; + + /** + * Whether the constant is impure, and not available in pure mode. + */ + bool impureOnly = false; }; #if HAVE_BOEHMGC @@ -513,18 +570,23 @@ public: */ std::shared_ptr<StaticEnv> staticBaseEnv; // !!! should be private + /** + * Name and documentation about every constant. + * + * Constants from primops are hard to crawl, and their docs will go + * here too. + */ + std::vector<std::pair<std::string, Constant>> constantInfos; + private: unsigned int baseEnvDispl = 0; void createBaseEnv(); - Value * addConstant(const std::string & name, Value & v); - - void addConstant(const std::string & name, Value * v); + Value * addConstant(const std::string & name, Value & v, Constant info); - Value * addPrimOp(const std::string & name, - size_t arity, PrimOpFun primOp); + void addConstant(const std::string & name, Value * v, Constant info); Value * addPrimOp(PrimOp && primOp); @@ -538,6 +600,10 @@ public: std::optional<std::string> name; size_t arity; std::vector<std::string> args; + /** + * Unlike the other `doc` fields in this file, this one should never be + * `null`. + */ const char * doc; }; @@ -740,7 +806,12 @@ struct EvalSettings : Config Setting<Strings> nixPath{ this, getDefaultNixPath(), "nix-path", - "List of directories to be searched for `<...>` file references."}; + R"( + List of directories to be searched for `<...>` file references + + In particular, outside of [pure evaluation mode](#conf-pure-evaluation), this determines the value of + [`builtins.nixPath`](@docroot@/language/builtin-constants.md#builtin-constants-nixPath). + )"}; Setting<bool> restrictEval{ this, false, "restrict-eval", diff --git a/src/libexpr/flake/flake.cc b/src/libexpr/flake/flake.cc index 60bb6a71e..5aa44d6a1 100644 --- a/src/libexpr/flake/flake.cc +++ b/src/libexpr/flake/flake.cc @@ -788,9 +788,6 @@ static RegisterPrimOp r2({ ```nix (builtins.getFlake "github:edolstra/dwarffs").rev ``` - - This function is only available if you enable the experimental feature - `flakes`. )", .fun = prim_getFlake, .experimentalFeature = Xp::Flakes, diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 9dd7bae02..5dfad470a 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -238,7 +238,7 @@ static void import(EvalState & state, const PosIdx pos, Value & vPath, Value * v } } -static RegisterPrimOp primop_scopedImport(RegisterPrimOp::Info { +static RegisterPrimOp primop_scopedImport(PrimOp { .name = "scopedImport", .arity = 2, .fun = [](EvalState & state, const PosIdx pos, Value * * args, Value & v) @@ -692,7 +692,7 @@ static void prim_genericClosure(EvalState & state, const PosIdx pos, Value * * a v.listElems()[n++] = i; } -static RegisterPrimOp primop_genericClosure(RegisterPrimOp::Info { +static RegisterPrimOp primop_genericClosure(PrimOp { .name = "__genericClosure", .args = {"attrset"}, .arity = 1, @@ -809,7 +809,7 @@ static void prim_addErrorContext(EvalState & state, const PosIdx pos, Value * * } } -static RegisterPrimOp primop_addErrorContext(RegisterPrimOp::Info { +static RegisterPrimOp primop_addErrorContext(PrimOp { .name = "__addErrorContext", .arity = 2, .fun = prim_addErrorContext, @@ -1400,7 +1400,7 @@ drvName, Bindings * attrs, Value & v) v.mkAttrs(result); } -static RegisterPrimOp primop_derivationStrict(RegisterPrimOp::Info { +static RegisterPrimOp primop_derivationStrict(PrimOp { .name = "derivationStrict", .arity = 1, .fun = prim_derivationStrict, @@ -1667,9 +1667,52 @@ static void prim_findFile(EvalState & state, const PosIdx pos, Value * * args, V v.mkPath(state.checkSourcePath(state.findFile(searchPath, path, pos))); } -static RegisterPrimOp primop_findFile(RegisterPrimOp::Info { +static RegisterPrimOp primop_findFile(PrimOp { .name = "__findFile", - .arity = 2, + .args = {"search path", "lookup path"}, + .doc = R"( + Look up the given path with the given search path. + + A search path is represented list of [attribute sets](./values.md#attribute-set) with two attributes, `prefix`, and `path`. + `prefix` is a relative path. + `path` denotes a file system location; the exact syntax depends on the command line interface. + + Examples of search path attribute sets: + + - ``` + { + prefix = "nixos-config"; + path = "/etc/nixos/configuration.nix"; + } + ``` + + - ``` + { + prefix = ""; + path = "/nix/var/nix/profiles/per-user/root/channels"; + } + ``` + + The lookup algorithm checks each entry until a match is found, returning a [path value](@docroot@/language/values.html#type-path) of the match. + + This is the process for each entry: + If the lookup path matches `prefix`, then the remainder of the lookup path (the "suffix") is searched for within the directory denoted by `patch`. + Note that the `path` may need to be downloaded at this point to look inside. + If the suffix is found inside that directory, then the entry is a match; + the combined absolute path of the directory (now downloaded if need be) and the suffix is returned. + + The syntax + + ```nix + <nixpkgs> + ``` + + is equivalent to: + + ```nix + builtins.findFile builtins.nixPath "nixpkgs" + ``` + )", .fun = prim_findFile, }); @@ -2388,7 +2431,7 @@ static void prim_unsafeGetAttrPos(EvalState & state, const PosIdx pos, Value * * state.mkPos(v, i->pos); } -static RegisterPrimOp primop_unsafeGetAttrPos(RegisterPrimOp::Info { +static RegisterPrimOp primop_unsafeGetAttrPos(PrimOp { .name = "__unsafeGetAttrPos", .arity = 2, .fun = prim_unsafeGetAttrPos, @@ -4061,10 +4104,10 @@ static RegisterPrimOp primop_splitVersion({ RegisterPrimOp::PrimOps * RegisterPrimOp::primOps; -RegisterPrimOp::RegisterPrimOp(Info && info) +RegisterPrimOp::RegisterPrimOp(PrimOp && primOp) { if (!primOps) primOps = new PrimOps; - primOps->push_back(std::move(info)); + primOps->push_back(std::move(primOp)); } @@ -4077,54 +4120,202 @@ void EvalState::createBaseEnv() /* `builtins' must be first! */ v.mkAttrs(buildBindings(128).finish()); - addConstant("builtins", v); + addConstant("builtins", v, { + .type = nAttrs, + .doc = R"( + Contains all the [built-in functions](@docroot@/language/builtins.md) and values. + + Since built-in functions were added over time, [testing for attributes](./operators.md#has-attribute) in `builtins` can be used for graceful fallback on older Nix installations: + + ```nix + # if hasContext is not available, we assume `s` has a context + if builtins ? hasContext then builtins.hasContext s else true + ``` + )", + }); v.mkBool(true); - addConstant("true", v); + addConstant("true", v, { + .type = nBool, + .doc = R"( + Primitive value. + + It can be returned by + [comparison operators](@docroot@/language/operators.md#Comparison) + and used in + [conditional expressions](@docroot@/language/constructs.md#Conditionals). + + The name `true` is not special, and can be shadowed: + + ```nix-repl + nix-repl> let true = 1; in true + 1 + ``` + )", + }); v.mkBool(false); - addConstant("false", v); + addConstant("false", v, { + .type = nBool, + .doc = R"( + Primitive value. + + It can be returned by + [comparison operators](@docroot@/language/operators.md#Comparison) + and used in + [conditional expressions](@docroot@/language/constructs.md#Conditionals). + + The name `false` is not special, and can be shadowed: + + ```nix-repl + nix-repl> let false = 1; in false + 1 + ``` + )", + }); v.mkNull(); - addConstant("null", v); + addConstant("null", v, { + .type = nNull, + .doc = R"( + Primitive value. + + The name `null` is not special, and can be shadowed: + + ```nix-repl + nix-repl> let null = 1; in null + 1 + ``` + )", + }); if (!evalSettings.pureEval) { v.mkInt(time(0)); - addConstant("__currentTime", v); + } + addConstant("__currentTime", v, { + .type = nInt, + .doc = R"( + Return the [Unix time](https://en.wikipedia.org/wiki/Unix_time) at first evaluation. + Repeated references to that name will re-use the initially obtained value. + + Example: + + ```console + $ nix repl + Welcome to Nix 2.15.1 Type :? for help. + nix-repl> builtins.currentTime + 1683705525 + + nix-repl> builtins.currentTime + 1683705525 + ``` + + The [store path](@docroot@/glossary.md#gloss-store-path) of a derivation depending on `currentTime` will differ for each evaluation, unless both evaluate `builtins.currentTime` in the same second. + )", + .impureOnly = true, + }); + + if (!evalSettings.pureEval) { v.mkString(settings.thisSystem.get()); - addConstant("__currentSystem", v); } + addConstant("__currentSystem", v, { + .type = nString, + .doc = R"( + The value of the [`system` configuration option](@docroot@/command-ref/conf-file.md#conf-pure-eval). + + It can be used to set the `system` attribute for [`builtins.derivation`](@docroot@/language/derivations.md) such that the resulting derivation can be built on the same system that evaluates the Nix expression: + + ```nix + builtins.derivation { + # ... + system = builtins.currentSystem; + } + ``` + + It can be overridden in order to create derivations for different system than the current one: + + ```console + $ nix-instantiate --system "mips64-linux" --eval --expr 'builtins.currentSystem' + "mips64-linux" + ``` + )", + .impureOnly = true, + }); v.mkString(nixVersion); - addConstant("__nixVersion", v); + addConstant("__nixVersion", v, { + .type = nString, + .doc = R"( + The version of Nix. + + For example, where the command line returns the current Nix version, + + ```shell-session + $ nix --version + nix (Nix) 2.16.0 + ``` + + the Nix language evaluator returns the same value: + + ```nix-repl + nix-repl> builtins.nixVersion + "2.16.0" + ``` + )", + }); v.mkString(store->storeDir); - addConstant("__storeDir", v); + addConstant("__storeDir", v, { + .type = nString, + .doc = R"( + Logical file system location of the [Nix store](@docroot@/glossary.md#gloss-store) currently in use. + + This value is determined by the `store` parameter in [Store URLs](@docroot@/command-ref/new-cli/nix3-help-stores.md): + + ```shell-session + $ nix-instantiate --store 'dummy://?store=/blah' --eval --expr builtins.storeDir + "/blah" + ``` + )", + }); /* Language version. This should be increased every time a new language feature gets added. It's not necessary to increase it when primops get added, because you can just use `builtins ? primOp' to check. */ v.mkInt(6); - addConstant("__langVersion", v); + addConstant("__langVersion", v, { + .type = nInt, + .doc = R"( + The current version of the Nix language. + )", + }); // Miscellaneous if (evalSettings.enableNativeCode) { - addPrimOp("__importNative", 2, prim_importNative); - addPrimOp("__exec", 1, prim_exec); + addPrimOp({ + .name = "__importNative", + .arity = 2, + .fun = prim_importNative, + }); + addPrimOp({ + .name = "__exec", + .arity = 1, + .fun = prim_exec, + }); } addPrimOp({ - .fun = evalSettings.traceVerbose ? prim_trace : prim_second, - .arity = 2, .name = "__traceVerbose", .args = { "e1", "e2" }, + .arity = 2, .doc = R"( Evaluate *e1* and print its abstract syntax representation on standard error if `--trace-verbose` is enabled. Then return *e2*. This function is useful for debugging. )", + .fun = evalSettings.traceVerbose ? prim_trace : prim_second, }); /* Add a value containing the current Nix expression search path. */ @@ -4136,26 +4327,46 @@ void EvalState::createBaseEnv() attrs.alloc("prefix").mkString(i.prefix); (v.listElems()[n++] = allocValue())->mkAttrs(attrs); } - addConstant("__nixPath", v); + addConstant("__nixPath", v, { + .type = nList, + .doc = R"( + The search path used to resolve angle bracket path lookups. + + Angle bracket expressions can be + [desugared](https://en.wikipedia.org/wiki/Syntactic_sugar) + using this and + [`builtins.findFile`](./builtins.html#builtins-findFile): + + ```nix + <nixpkgs> + ``` + + is equivalent to: + + ```nix + builtins.findFile builtins.nixPath "nixpkgs" + ``` + )", + }); if (RegisterPrimOp::primOps) for (auto & primOp : *RegisterPrimOp::primOps) - if (!primOp.experimentalFeature - || experimentalFeatureSettings.isEnabled(*primOp.experimentalFeature)) + if (experimentalFeatureSettings.isEnabled(primOp.experimentalFeature)) { - addPrimOp({ - .fun = primOp.fun, - .arity = std::max(primOp.args.size(), primOp.arity), - .name = primOp.name, - .args = primOp.args, - .doc = primOp.doc, - }); + auto primOpAdjusted = primOp; + primOpAdjusted.arity = std::max(primOp.args.size(), primOp.arity); + addPrimOp(std::move(primOpAdjusted)); } /* Add a wrapper around the derivation primop that computes the - `drvPath' and `outPath' attributes lazily. */ + `drvPath' and `outPath' attributes lazily. + + Null docs because it is documented separately. + */ auto vDerivation = allocValue(); - addConstant("derivation", vDerivation); + addConstant("derivation", vDerivation, { + .type = nFunction, + }); /* Now that we've added all primops, sort the `builtins' set, because attribute lookups expect it to be sorted. */ diff --git a/src/libexpr/primops.hh b/src/libexpr/primops.hh index 73b7b866c..930e7f32a 100644 --- a/src/libexpr/primops.hh +++ b/src/libexpr/primops.hh @@ -10,17 +10,7 @@ namespace nix { struct RegisterPrimOp { - struct Info - { - std::string name; - std::vector<std::string> args; - size_t arity = 0; - const char * doc; - PrimOpFun fun; - std::optional<ExperimentalFeature> experimentalFeature; - }; - - typedef std::vector<Info> PrimOps; + typedef std::vector<PrimOp> PrimOps; static PrimOps * primOps; /** @@ -28,7 +18,7 @@ struct RegisterPrimOp * will get called during EvalState initialization, so there * may be primops not yet added and builtins is not yet sorted. */ - RegisterPrimOp(Info && info); + RegisterPrimOp(PrimOp && primOp); }; /* These primops are disabled without enableNativeCode, but plugins diff --git a/src/libexpr/primops/fetchClosure.cc b/src/libexpr/primops/fetchClosure.cc index 4cf1f1e0b..bae849f61 100644 --- a/src/libexpr/primops/fetchClosure.cc +++ b/src/libexpr/primops/fetchClosure.cc @@ -154,9 +154,6 @@ static RegisterPrimOp primop_fetchClosure({ specifying a binary cache from which the path can be fetched. Also, requiring a content-addressed final store path avoids the need for users to configure binary cache public keys. - - This function is only available if you enable the experimental - feature `fetch-closure`. )", .fun = prim_fetchClosure, .experimentalFeature = Xp::FetchClosure, diff --git a/src/libexpr/value.hh b/src/libexpr/value.hh index 89c0c36fd..44c8071c2 100644 --- a/src/libexpr/value.hh +++ b/src/libexpr/value.hh @@ -218,8 +218,11 @@ public: /** * Returns the normal type of a Value. This only returns nThunk if * the Value hasn't been forceValue'd + * + * @param invalidIsThunk Instead of aborting an an invalid (probably + * 0, so uninitialized) internal type, return `nThunk`. */ - inline ValueType type() const + inline ValueType type(bool invalidIsThunk = false) const { switch (internalType) { case tInt: return nInt; @@ -234,7 +237,10 @@ public: case tFloat: return nFloat; case tThunk: case tApp: case tBlackhole: return nThunk; } - abort(); + if (invalidIsThunk) + return nThunk; + else + abort(); } /** diff --git a/src/nix/main.cc b/src/nix/main.cc index ce0bed2a3..650c79d14 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -352,7 +352,7 @@ void mainWrapped(int argc, char * * argv) return; } - if (argc == 2 && std::string(argv[1]) == "__dump-builtins") { + if (argc == 2 && std::string(argv[1]) == "__dump-language") { experimentalFeatureSettings.experimentalFeatures = { Xp::Flakes, Xp::FetchClosure, @@ -360,17 +360,34 @@ void mainWrapped(int argc, char * * argv) evalSettings.pureEval = false; EvalState state({}, openStore("dummy://")); auto res = nlohmann::json::object(); - auto builtins = state.baseEnv.values[0]->attrs; - for (auto & builtin : *builtins) { - auto b = nlohmann::json::object(); - if (!builtin.value->isPrimOp()) continue; - auto primOp = builtin.value->primOp; - if (!primOp->doc) continue; - b["arity"] = primOp->arity; - b["args"] = primOp->args; - b["doc"] = trim(stripIndentation(primOp->doc)); - res[state.symbols[builtin.name]] = std::move(b); - } + res["builtins"] = ({ + auto builtinsJson = nlohmann::json::object(); + auto builtins = state.baseEnv.values[0]->attrs; + for (auto & builtin : *builtins) { + auto b = nlohmann::json::object(); + if (!builtin.value->isPrimOp()) continue; + auto primOp = builtin.value->primOp; + if (!primOp->doc) continue; + b["arity"] = primOp->arity; + b["args"] = primOp->args; + b["doc"] = trim(stripIndentation(primOp->doc)); + b["experimental-feature"] = primOp->experimentalFeature; + builtinsJson[state.symbols[builtin.name]] = std::move(b); + } + std::move(builtinsJson); + }); + res["constants"] = ({ + auto constantsJson = nlohmann::json::object(); + for (auto & [name, info] : state.constantInfos) { + auto c = nlohmann::json::object(); + if (!info.doc) continue; + c["doc"] = trim(stripIndentation(info.doc)); + c["type"] = showType(info.type, false); + c["impure-only"] = info.impureOnly; + constantsJson[name] = std::move(c); + } + std::move(constantsJson); + }); logger->cout("%s", res); return; } |