aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJohn Ericson <John.Ericson@Obsidian.Systems>2023-05-13 13:52:45 -0400
committerJohn Ericson <John.Ericson@Obsidian.Systems>2023-06-27 09:37:54 -0400
commit22b278e011ab9c1328749a126514c57b89a39173 (patch)
tree0920c88b6bc996502f5ebf153fcb4c8956321e6e /src
parentd40f0e534d657e619634ba204fb8970f4a01fbc5 (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.cc37
-rw-r--r--src/libexpr/eval.hh87
-rw-r--r--src/libexpr/flake/flake.cc3
-rw-r--r--src/libexpr/primops.cc279
-rw-r--r--src/libexpr/primops.hh14
-rw-r--r--src/libexpr/primops/fetchClosure.cc3
-rw-r--r--src/libexpr/value.hh10
-rw-r--r--src/nix/main.cc41
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;
}