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
1 change: 0 additions & 1 deletion maintainers/flake-module.nix
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@
''^src/libexpr/nixexpr\.cc$''
''^src/libexpr/nixexpr\.hh$''
''^src/libexpr/parser-state\.hh$''
''^src/libexpr/pos-table\.hh$''
''^src/libexpr/primops\.cc$''
''^src/libexpr/primops\.hh$''
''^src/libexpr/primops/context\.cc$''
Expand Down
11 changes: 4 additions & 7 deletions src/libcmd/repl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,13 @@ static std::ostream & showDebugTrace(std::ostream & out, const PosTable & positi
out << ANSI_RED "error: " << ANSI_NORMAL;
out << dt.hint.str() << "\n";

// prefer direct pos, but if noPos then try the expr.
auto pos = dt.pos
? dt.pos
: positions[dt.expr.getPos() ? dt.expr.getPos() : noPos];
auto pos = dt.getPos(positions);

if (pos) {
out << *pos;
if (auto loc = pos->getCodeLines()) {
out << pos;
if (auto loc = pos.getCodeLines()) {
out << "\n";
printCodeLines(out, "", *pos, *loc);
printCodeLines(out, "", pos, *loc);
out << "\n";
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/libexpr/eval-error.cc
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ EvalErrorBuilder<T> & EvalErrorBuilder<T>::withFrame(const Env & env, const Expr
// TODO: check compatibility with nested debugger calls.
// TODO: What side-effects??
error.state.debugTraces.push_front(DebugTrace{
.pos = error.state.positions[expr.getPos()],
.pos = expr.getPos(),
.expr = expr,
.env = env,
.hint = HintFmt("Fake frame for debugging purposes"),
Expand Down
44 changes: 24 additions & 20 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -771,18 +771,26 @@ void EvalState::runDebugRepl(const Error * error, const Env & env, const Expr &
if (!debugRepl || inDebugger)
return;

auto dts =
error && expr.getPos()
? std::make_unique<DebugTraceStacker>(
*this,
DebugTrace {
.pos = error->info().pos ? error->info().pos : positions[expr.getPos()],
auto dts = [&]() -> std::unique_ptr<DebugTraceStacker> {
if (error && expr.getPos()) {
auto trace = DebugTrace{
.pos = [&]() -> std::variant<Pos, PosIdx> {
if (error->info().pos) {
if (auto * pos = error->info().pos.get())
return *pos;
return noPos;
}
return expr.getPos();
}(),
.expr = expr,
.env = env,
.hint = error->info().msg,
.isError = true
})
: nullptr;
.isError = true};

return std::make_unique<DebugTraceStacker>(*this, std::move(trace));
}
return nullptr;
}();

if (error)
{
Expand Down Expand Up @@ -827,7 +835,7 @@ static std::unique_ptr<DebugTraceStacker> makeDebugTraceStacker(
EvalState & state,
Expr & expr,
Env & env,
std::shared_ptr<Pos> && pos,
std::variant<Pos, PosIdx> pos,
const Args & ... formatArgs)
{
return std::make_unique<DebugTraceStacker>(state,
Expand Down Expand Up @@ -1104,7 +1112,7 @@ void EvalState::evalFile(const SourcePath & path, Value & v, bool mustBeTrivial)
*this,
*e,
this->baseEnv,
e->getPos() ? std::make_shared<Pos>(positions[e->getPos()]) : nullptr,
e->getPos(),
"while evaluating the file '%1%':", resolvedPath.to_string())
: nullptr;

Expand Down Expand Up @@ -1330,9 +1338,7 @@ void ExprLet::eval(EvalState & state, Env & env, Value & v)
state,
*this,
env2,
getPos()
? std::make_shared<Pos>(state.positions[getPos()])
: nullptr,
getPos(),
"while evaluating a '%1%' expression",
"let"
)
Expand Down Expand Up @@ -1401,7 +1407,7 @@ void ExprSelect::eval(EvalState & state, Env & env, Value & v)
state,
*this,
env,
state.positions[getPos()],
getPos(),
"while evaluating the attribute '%1%'",
showAttrPath(state, env, attrPath))
: nullptr;
Expand Down Expand Up @@ -1602,7 +1608,7 @@ void EvalState::callFunction(Value & fun, std::span<Value *> args, Value & vRes,
try {
auto dts = debugRepl
? makeDebugTraceStacker(
*this, *lambda.body, env2, positions[lambda.pos],
*this, *lambda.body, env2, lambda.pos,
"while calling %s",
lambda.name
? concatStrings("'", symbols[lambda.name], "'")
Expand Down Expand Up @@ -1737,9 +1743,7 @@ void ExprCall::eval(EvalState & state, Env & env, Value & v)
state,
*this,
env,
getPos()
? std::make_shared<Pos>(state.positions[getPos()])
: nullptr,
getPos(),
"while calling a function"
)
: nullptr;
Expand Down Expand Up @@ -2123,7 +2127,7 @@ void EvalState::forceValueDeep(Value & v)
try {
// If the value is a thunk, we're evaling. Otherwise no trace necessary.
auto dts = debugRepl && i.value->isThunk()
? makeDebugTraceStacker(*this, *i.value->payload.thunk.expr, *i.value->payload.thunk.env, positions[i.pos],
? makeDebugTraceStacker(*this, *i.value->payload.thunk.expr, *i.value->payload.thunk.env, i.pos,
"while evaluating the attribute '%1%'", symbols[i.name])
: nullptr;

Expand Down
19 changes: 18 additions & 1 deletion src/libexpr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,28 @@ struct RegexCache;
std::shared_ptr<RegexCache> makeRegexCache();

struct DebugTrace {
std::shared_ptr<Pos> pos;
/* WARNING: Converting PosIdx -> Pos should be done with extra care. This is
due to the fact that operator[] of PosTable is incredibly expensive. */
std::variant<Pos, PosIdx> pos;
const Expr & expr;
const Env & env;
HintFmt hint;
bool isError;

Pos getPos(const PosTable & table) const
{
return std::visit(
overloaded{
[&](PosIdx idx) {
// Prefer direct pos, but if noPos then try the expr.
if (!idx)
idx = expr.getPos();
return table[idx];
},
[&](Pos pos) { return pos; },
},
pos);
}
};

class EvalState : public std::enable_shared_from_this<EvalState>
Expand Down
2 changes: 0 additions & 2 deletions src/libexpr/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,6 @@ headers = [config_h] + files(
# internal: 'lexer-helpers.hh',
'nixexpr.hh',
'parser-state.hh',
'pos-idx.hh',
'pos-table.hh',
'primops.hh',
'print-ambiguous.hh',
'print-options.hh',
Expand Down
35 changes: 0 additions & 35 deletions src/libexpr/nixexpr.cc
Original file line number Diff line number Diff line change
Expand Up @@ -601,41 +601,6 @@ void ExprLambda::setDocComment(DocComment docComment) {
}
};



/* Position table. */

Pos PosTable::operator[](PosIdx p) const
{
auto origin = resolve(p);
if (!origin)
return {};

const auto offset = origin->offsetOf(p);

Pos result{0, 0, origin->origin};
auto lines = this->lines.lock();
auto linesForInput = (*lines)[origin->offset];

if (linesForInput.empty()) {
auto source = result.getSource().value_or("");
const char * begin = source.data();
for (Pos::LinesIterator it(source), end; it != end; it++)
linesForInput.push_back(it->data() - begin);
if (linesForInput.empty())
linesForInput.push_back(0);
}
// as above: the first line starts at byte 0 and is always present
auto lineStartOffset = std::prev(
std::upper_bound(linesForInput.begin(), linesForInput.end(), offset));

result.line = 1 + (lineStartOffset - linesForInput.begin());
result.column = 1 + (offset - *lineStartOffset);
return result;
}



/* Symbol table. */

size_t SymbolTable::totalSize() const
Expand Down
8 changes: 8 additions & 0 deletions src/libutil/error.hh
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ struct LinesOfCode {
std::optional<std::string> nextLineOfCode;
};

/* NOTE: position.hh recursively depends on source-path.hh -> source-accessor.hh
-> hash.hh -> config.hh -> experimental-features.hh -> error.hh -> Pos.
There are other such cycles.
Thus, Pos has to be an incomplete type in this header. But since ErrorInfo/Trace
have to refer to Pos, they have to use pointer indirection via std::shared_ptr
to break the recursive header dependency.
FIXME: Untangle this mess. Should there be AbstractPos as there used to be before
4feb7d9f71? */
Comment on lines +53 to +60
Copy link
Member

Choose a reason for hiding this comment

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

I am guessing experimental-features.hh is the easy thing to make more abstract

I was planning on doing that anyways for a new settings system.

Copy link
Member

Choose a reason for hiding this comment

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

Forgot to write down more thoughts.

While it would be good to slim down nix-util and have more things in libraries with well-defined scopes, I think it's ok to consider Pos to be part of a text handling component; not the evaluator. This frees it up to be used in nix.conf logic and other file types.

struct Pos;

void printCodeLines(std::ostream & out,
Expand Down
3 changes: 3 additions & 0 deletions src/libutil/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ sources = files(
'memory-source-accessor.cc',
'mounted-source-accessor.cc',
'position.cc',
'pos-table.cc',
'posix-source-accessor.cc',
'references.cc',
'serialise.cc',
Expand Down Expand Up @@ -225,6 +226,8 @@ headers = [config_h] + files(
'muxable-pipe.hh',
'os-string.hh',
'pool.hh',
'pos-idx.hh',
'pos-table.hh',
'position.hh',
'posix-source-accessor.hh',
'processes.hh',
Expand Down
1 change: 1 addition & 0 deletions src/libexpr/pos-idx.hh → src/libutil/pos-idx.hh
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#pragma once
///@file

#include <cinttypes>
#include <functional>
Expand Down
37 changes: 37 additions & 0 deletions src/libutil/pos-table.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#include "pos-table.hh"

#include <algorithm>

namespace nix {

/* Position table. */

Pos PosTable::operator[](PosIdx p) const
{
auto origin = resolve(p);
if (!origin)
return {};

const auto offset = origin->offsetOf(p);

Pos result{0, 0, origin->origin};
auto lines = this->lines.lock();
auto linesForInput = (*lines)[origin->offset];

if (linesForInput.empty()) {
auto source = result.getSource().value_or("");
const char * begin = source.data();
for (Pos::LinesIterator it(source), end; it != end; it++)
linesForInput.push_back(it->data() - begin);
if (linesForInput.empty())
linesForInput.push_back(0);
}
// as above: the first line starts at byte 0 and is always present
auto lineStartOffset = std::prev(std::upper_bound(linesForInput.begin(), linesForInput.end(), offset));

result.line = 1 + (lineStartOffset - linesForInput.begin());
result.column = 1 + (offset - *lineStartOffset);
return result;
}

}
21 changes: 18 additions & 3 deletions src/libexpr/pos-table.hh → src/libutil/pos-table.hh
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#pragma once
///@file

#include <cstdint>
#include <vector>
Expand All @@ -18,9 +19,12 @@ public:
private:
uint32_t offset;

Origin(Pos::Origin origin, uint32_t offset, size_t size):
offset(offset), origin(origin), size(size)
{}
Origin(Pos::Origin origin, uint32_t offset, size_t size)
: offset(offset)
, origin(origin)
, size(size)
{
}

public:
const Pos::Origin origin;
Expand Down Expand Up @@ -72,6 +76,17 @@ public:
return PosIdx(1 + origin.offset + offset);
}

/**
* Convert a byte-offset PosIdx into a Pos with line/column information.
*
* @param p Byte offset into the virtual concatenation of all parsed contents
* @return Position
*
* @warning Very expensive to call, as this has to read the entire source
* into memory each time. Call this only if absolutely necessary. Prefer
* to keep PosIdx around instead of needlessly converting it into Pos by
* using this lookup method.
*/
Copy link
Member

Choose a reason for hiding this comment

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

thought:
Additionally it may make sense to keep a number of recent files in memory.
I doubt that it'd be noticeable after your fixes though.

Copy link
Contributor Author

@xokdvium xokdvium Mar 13, 2025

Choose a reason for hiding this comment

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

Yeah, using an LRUCache might make sense if we want to speed up the debugger eval even further. Though I don't think it's strictly necessary for this PR. There's much mess to untangle and premature optimizations might introduce unnecessary complications. ~5 minor releases seem to have this performance problem, so sticking to several orders of magnitute speedup should suffice for now.

In my profiling operator[] only gets called 3 times for valgrind --tool=callgrind src/nix/nix eval nixpkgs#hello --impure --ignore-try --no-eval-cache --debugger: (after this PR)

image

Pos operator[](PosIdx p) const;

Pos::Origin originOf(PosIdx p) const
Expand Down
7 changes: 7 additions & 0 deletions src/libutil/position.cc
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ std::optional<std::string> Pos::getSource() const
}, origin);
}

std::optional<SourcePath> Pos::getSourcePath() const
{
if (auto * path = std::get_if<SourcePath>(&origin))
return *path;
return std::nullopt;
}

void Pos::print(std::ostream & out, bool showOrigin) const
{
if (showOrigin) {
Expand Down
5 changes: 2 additions & 3 deletions src/libutil/position.hh
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ struct Pos

explicit operator bool() const { return line > 0; }

/* TODO: Why std::shared_ptr<Pos> and not std::shared_ptr<const Pos>? */
operator std::shared_ptr<Pos>() const;

/**
Expand All @@ -69,9 +70,7 @@ struct Pos
/**
* Get the SourcePath, if the source was loaded from a file.
*/
std::optional<SourcePath> getSourcePath() const {
return *std::get_if<SourcePath>(&origin);
}
std::optional<SourcePath> getSourcePath() const;

struct LinesIterator {
using difference_type = size_t;
Expand Down
Loading