Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions doc/manual/rl-next/repl-doc-renders-doc-comments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
synopsis: "`nix-repl`'s `:doc` shows documentation comments"
significance: significant
issues:
- 3904
- 10771
prs:
- 1652
- 9054
- 11072
---

`nix repl` has a `:doc` command that previously only rendered documentation for internally defined functions.
This feature has been extended to also render function documentation comments, in accordance with [RFC 145].

Example:

```
nix-repl> :doc lib.toFunction
Function toFunction
… defined at /home/user/h/nixpkgs/lib/trivial.nix:1072:5

Turns any non-callable values into constant functions. Returns
callable values as is.

Inputs

v

: Any value

Examples

:::{.example}

## lib.trivial.toFunction usage example

| nix-repl> lib.toFunction 1 2
| 1
|
| nix-repl> lib.toFunction (x: x + 1) 2
| 3

:::
```

Known limitations:
- It does not render documentation for "formals", such as `{ /** the value to return */ x, ... }: x`.
- Some extensions to markdown are not yet supported, as you can see in the example above.

We'd like to acknowledge Yingchi Long for proposing a proof of concept for this functionality in [#9054](https://github.com/NixOS/nix/pull/9054), as well as @sternenseemann and Johannes Kirschbauer for their contributions, proposals, and their work on [RFC 145].

[RFC 145]: https://github.com/NixOS/rfcs/pull/145
2 changes: 1 addition & 1 deletion doc/manual/src/contributing/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ A environment variables that Google Test accepts are also worth knowing:
Putting the two together, one might run

```bash
GTEST_BREIF=1 GTEST_FILTER='ErrorTraceTest.*' meson test nix-expr-tests -v
GTEST_BRIEF=1 GTEST_FILTER='ErrorTraceTest.*' meson test nix-expr-tests -v
```

for short but comprensive output.
Expand Down
2 changes: 1 addition & 1 deletion src/libcmd/repl-interacter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ static int listPossibleCallback(char * s, char *** avp)
{
auto possible = curRepl->completePrefix(s);

if (possible.size() > (INT_MAX / sizeof(char *)))
if (possible.size() > (std::numeric_limits<int>::max() / sizeof(char *)))
throw Error("too many completions");

int ac = 0;
Expand Down
46 changes: 46 additions & 0 deletions src/libcmd/repl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "local-fs-store.hh"
#include "print.hh"
#include "ref.hh"
#include "value.hh"

#if HAVE_BOEHMGC
#define GC_INCLUDE_NEW
Expand Down Expand Up @@ -616,6 +617,38 @@ ProcessLineResult NixRepl::processLine(std::string line)

else if (command == ":doc") {
Value v;

auto expr = parseString(arg);
std::string fallbackName;
PosIdx fallbackPos;
DocComment fallbackDoc;
if (auto select = dynamic_cast<ExprSelect *>(expr)) {
Value vAttrs;
auto name = select->evalExceptFinalSelect(*state, *env, vAttrs);
fallbackName = state->symbols[name];

state->forceAttrs(vAttrs, noPos, "while evaluating an attribute set to look for documentation");
auto attrs = vAttrs.attrs();
assert(attrs);
auto attr = attrs->get(name);
if (!attr) {
// When missing, trigger the normal exception
// e.g. :doc builtins.foo
// behaves like
// nix-repl> builtins.foo
// error: attribute 'foo' missing
evalString(arg, v);
assert(false);
}
if (attr->pos) {
fallbackPos = attr->pos;
fallbackDoc = state->getDocCommentForPos(fallbackPos);
}

} else {
evalString(arg, v);
}

evalString(arg, v);
if (auto doc = state->getDoc(v)) {
std::string markdown;
Expand All @@ -633,6 +666,19 @@ ProcessLineResult NixRepl::processLine(std::string line)
markdown += stripIndentation(doc->doc);

logger->cout(trim(renderMarkdownToTerminal(markdown)));
} else if (fallbackPos) {
std::stringstream ss;
ss << "Attribute `" << fallbackName << "`\n\n";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ss << "Attribute `" << fallbackName << "`\n\n";
ss << fmt("Attribute `%s`\n\n", fallbackName);

etc.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried this, but unfortunately it doesn't work, as these go into the markdown renderer, instead of the terminal.

nix-repl> :doc lib.version
Attribute '[35;1mversion[0m'

    … defined at [35;1m/home/user/h/nixpkgs/lib/default.nix:73:40[0m

We could switch that to go direct to the terminal, but then we should do the same for the primops, to get a consistent look.

Could be done in a follow-up and I've left a reverted commit to cherry pick.

ss << " … defined at " << state->positions[fallbackPos] << "\n\n";
if (fallbackDoc) {
ss << fallbackDoc.getInnerText(state->positions);
} else {
ss << "No documentation found.\n\n";
}

auto markdown = ss.str();
logger->cout(trim(renderMarkdownToTerminal(markdown)));

} else
throw Error("value does not have documentation");
}
Expand Down
90 changes: 89 additions & 1 deletion src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,54 @@ std::optional<EvalState::Doc> EvalState::getDoc(Value & v)
.doc = doc,
};
}
if (v.isLambda()) {
auto exprLambda = v.payload.lambda.fun;

std::stringstream s(std::ios_base::out);
std::string name;
auto pos = positions[exprLambda->getPos()];
std::string docStr;

if (exprLambda->name) {
name = symbols[exprLambda->name];
}

if (exprLambda->docComment) {
docStr = exprLambda->docComment.getInnerText(positions);
}

if (name.empty()) {
s << "Function ";
}
else {
s << "Function `" << name << "`";
if (pos)
s << "\\\n … " ;
else
s << "\\\n";
}
if (pos) {
s << "defined at " << pos;
}
if (!docStr.empty()) {
s << "\n\n";
}

s << docStr;

s << '\0'; // for making a c string below
std::string ss = s.str();

return Doc {
.pos = pos,
.name = name,
.arity = 0, // FIXME: figure out how deep by syntax only? It's not semantically useful though...
.args = {},
.doc =
// FIXME: this leaks; make the field std::string?
strdup(ss.data()),
};
}
return {};
}

Expand Down Expand Up @@ -1367,6 +1415,22 @@ void ExprSelect::eval(EvalState & state, Env & env, Value & v)
v = *vAttrs;
}

Symbol ExprSelect::evalExceptFinalSelect(EvalState & state, Env & env, Value & attrs)
{
Value vTmp;
Symbol name = getName(attrPath[attrPath.size() - 1], state, env);

if (attrPath.size() == 1) {
e->eval(state, env, vTmp);
} else {
ExprSelect init(*this);
init.attrPath.pop_back();
init.eval(state, env, vTmp);
}
attrs = vTmp;
return name;
}


void ExprOpHasAttr::eval(EvalState & state, Env & env, Value & v)
{
Expand Down Expand Up @@ -2828,13 +2892,37 @@ Expr * EvalState::parse(
const SourcePath & basePath,
std::shared_ptr<StaticEnv> & staticEnv)
{
auto result = parseExprFromBuf(text, length, origin, basePath, symbols, settings, positions, rootFS, exprSymbols);
DocCommentMap tmpDocComments; // Only used when not origin is not a SourcePath
DocCommentMap *docComments = &tmpDocComments;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
DocCommentMap *docComments = &tmpDocComments;
auto docComments = &tmpDocComments;


if (auto sourcePath = std::get_if<SourcePath>(&origin)) {
auto [it, _] = positionToDocComment.try_emplace(*sourcePath);
docComments = &it->second;
}

auto result = parseExprFromBuf(text, length, origin, basePath, symbols, settings, positions, *docComments, rootFS, exprSymbols);

result->bindVars(*this, staticEnv);

return result;
}

DocComment EvalState::getDocCommentForPos(PosIdx pos)
{
auto pos2 = positions[pos];
auto path = pos2.getSourcePath();
if (!path)
return {};

auto table = positionToDocComment.find(*path);
if (table == positionToDocComment.end())
return {};

auto it = table->second.find(pos);
if (it == table->second.end())
return {};
return it->second;
}

std::string ExternalValueBase::coerceToString(EvalState & state, const PosIdx & pos, NixStringContext & context, bool copyMore, bool copyToStore) const
{
Expand Down
10 changes: 10 additions & 0 deletions src/libexpr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ struct Constant
typedef std::map<std::string, Value *> ValMap;
#endif

typedef std::map<PosIdx, DocComment> DocCommentMap;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
typedef std::map<PosIdx, DocComment> DocCommentMap;
typedef std::unordered_map<PosIdx, DocComment> DocCommentMap;

(unless this needs to be sorted)


struct Env
{
Env * up;
Expand Down Expand Up @@ -329,6 +331,12 @@ private:
#endif
FileEvalCache fileEvalCache;

/**
* Associate source positions of certain AST nodes with their preceding doc comment, if they have one.
* Grouped by file.
*/
std::map<SourcePath, DocCommentMap> positionToDocComment;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you like to inspect memory impact of this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is hard to assess until doc comments are used more widely in real code.
Performance is definitely worth considering, but I think for now it's more effective to use tooling to figure out where most memory is spent, because unlikely to be here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
std::map<SourcePath, DocCommentMap> positionToDocComment;
std::unordered_map<SourcePath, DocCommentMap> positionToDocComment;

(See #11092.)


LookupPath lookupPath;

std::map<std::string, std::optional<std::string>> lookupPathResolved;
Expand Down Expand Up @@ -771,6 +779,8 @@ public:
std::string_view pathArg,
PosIdx pos);

DocComment getDocCommentForPos(PosIdx pos);

private:

/**
Expand Down
30 changes: 30 additions & 0 deletions src/libexpr/lexer-helpers.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#include "lexer-tab.hh"
#include "lexer-helpers.hh"
#include "parser-tab.hh"

void nix::lexer::internal::initLoc(YYLTYPE * loc)
{
loc->beginOffset = loc->endOffset = 0;
}

void nix::lexer::internal::adjustLoc(yyscan_t yyscanner, YYLTYPE * loc, const char * s, size_t len)
{
loc->stash();

LexerState & lexerState = *yyget_extra(yyscanner);

if (lexerState.docCommentDistance == 1) {
// Preceding token was a doc comment.
ParserLocation doc;
doc.beginOffset = lexerState.lastDocCommentLoc.beginOffset;
ParserLocation docEnd;
docEnd.beginOffset = lexerState.lastDocCommentLoc.endOffset;
DocComment docComment{lexerState.at(doc), lexerState.at(docEnd)};
PosIdx locPos = lexerState.at(*loc);
lexerState.positionToDocComment.emplace(locPos, docComment);
}
lexerState.docCommentDistance++;

loc->beginOffset = loc->endOffset;
loc->endOffset += len;
}
9 changes: 9 additions & 0 deletions src/libexpr/lexer-helpers.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once

namespace nix::lexer::internal {

void initLoc(YYLTYPE * loc);

void adjustLoc(yyscan_t yyscanner, YYLTYPE * loc, const char * s, size_t len);

} // namespace nix::lexer
Loading