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
100 changes: 100 additions & 0 deletions src/libexpr-tests/lazy-fetcher-attr.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#include <gtest/gtest.h>

#include "nix/expr/fetch-tree.hh"
#include "nix/expr/tests/libexpr.hh"
#include "nix/fetchers/attrs.hh"
#include "nix/fetchers/fetchers.hh"
#include "nix/store/path.hh"

namespace nix {

class LazyFetcherAttrTest : public LibExprTest
{
protected:
StorePath dummyPath()
{
return StorePath{"g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-test"};
}
};

TEST_F(LazyFetcherAttrTest, nonLazyAttrProducesImmediateValue)
{
fetchers::Input input;
input.attrs.insert_or_assign("type", std::string("git"));
input.attrs.insert_or_assign("revCount", uint64_t(5));

Value v;
emitTreeAttrs(state, dummyPath(), input, v, false, false);
state.forceValue(v, noPos);

auto * rcAttr = v.attrs()->get(state.symbols.create("revCount"));
ASSERT_NE(rcAttr, nullptr);
state.forceValue(*rcAttr->value, noPos);
EXPECT_EQ(rcAttr->value->integer().value, 5);
}

TEST_F(LazyFetcherAttrTest, lazyAttrProducesThunk)
{
int calls = 0;
fetchers::Input input;
input.attrs.insert_or_assign("type", std::string("git"));
input.attrs.insert_or_assign(
"revCount",
fetchers::LazyAttr(
make_ref<fetchers::LazyAttrComputation>(
fetchers::LazyAttrComputation{.compute = [&calls]() -> fetchers::ResolvedAttr {
calls++;
return uint64_t(42);
}})));

Value v;
emitTreeAttrs(state, dummyPath(), input, v, false, false);
state.forceValue(v, noPos);

auto * rcAttr = v.attrs()->get(state.symbols.create("revCount"));
ASSERT_NE(rcAttr, nullptr);

// Not yet forced, so the lazy function should not have been called
EXPECT_EQ(calls, 0);

// Force the thunk
state.forceValue(*rcAttr->value, noPos);
EXPECT_EQ(rcAttr->value->integer().value, 42);
EXPECT_EQ(calls, 1);
}

TEST_F(LazyFetcherAttrTest, lazyFunctionOnlyCalledOnAccess)
{
int calls = 0;
fetchers::Input input;
input.attrs.insert_or_assign("type", std::string("git"));
input.attrs.insert_or_assign("lastModified", uint64_t(1000));
input.attrs.insert_or_assign(
"revCount",
fetchers::LazyAttr(
make_ref<fetchers::LazyAttrComputation>(
fetchers::LazyAttrComputation{.compute = [&calls]() -> fetchers::ResolvedAttr {
calls++;
return uint64_t(99);
}})));

Value v;
emitTreeAttrs(state, dummyPath(), input, v, false, false);
state.forceValue(v, noPos);

// Access lastModified, so should not trigger lazy revCount
auto * lmAttr = v.attrs()->get(state.symbols.create("lastModified"));
ASSERT_NE(lmAttr, nullptr);
state.forceValue(*lmAttr->value, noPos);
EXPECT_EQ(lmAttr->value->integer().value, 1000);
EXPECT_EQ(calls, 0);

// Now access revCount
auto * rcAttr = v.attrs()->get(state.symbols.create("revCount"));
ASSERT_NE(rcAttr, nullptr);
state.forceValue(*rcAttr->value, noPos);
EXPECT_EQ(rcAttr->value->integer().value, 99);
EXPECT_EQ(calls, 1);
}

} // namespace nix
1 change: 1 addition & 0 deletions src/libexpr-tests/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ sources = files(
'error_traces.cc',
'eval.cc',
'json.cc',
'lazy-fetcher-attr.cc',
'main.cc',
'nix_api_expr.cc',
'nix_api_external.cc',
Expand Down
18 changes: 18 additions & 0 deletions src/libexpr/include/nix/expr/fetch-tree.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#pragma once

#include "nix/expr/eval.hh"

namespace nix {

/**
* Convert a libfetchers `Input` to libexpr `Value`.
*/
void emitTreeAttrs(
EvalState & state,
const StorePath & storePath,
const fetchers::Input & input,
Value & v,
bool emptyRevFallback = false,
bool forceDirty = false);

} // namespace nix
1 change: 1 addition & 0 deletions src/libexpr/include/nix/expr/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ headers = [ config_pub_h ] + files(
'eval-profiler.hh',
'eval-settings.hh',
'eval.hh',
'fetch-tree.hh',
'function-trace.hh',
'gc-small-vector.hh',
'get-drvs.hh',
Expand Down
95 changes: 94 additions & 1 deletion src/libexpr/primops/fetchTree.cc
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#include "nix/expr/value.hh"
#include "nix/fetchers/attrs.hh"
#include "nix/expr/primops.hh"
#include "nix/expr/eval-inline.hh"
#include "nix/expr/eval-settings.hh"
#include "nix/expr/fetch-tree.hh"
#include "nix/store/store-api.hh"
#include "nix/fetchers/fetchers.hh"
#include "nix/store/filetransfer.hh"
Expand All @@ -19,6 +21,95 @@

namespace nix {

/**
* Adapter for putting libfetchers data into a thunk closure.
* Used as the argument to prim_forceLazyFetcherAttr in a lazy apply thunk.
*/
class LazyFetcherAttr : public ExternalValueBase, public gc_cleanup
{
fetchers::LazyAttr lazy;

public:
LazyFetcherAttr(fetchers::LazyAttr lazy)
: lazy(std::move(lazy))
{
}

fetchers::ResolvedAttr force()
{
return lazy->compute();
}

protected:
std::ostream & print(std::ostream & str) const override
{
unreachable();
}

public:
std::string showType() const override
{
unreachable();
}

std::string typeOf() const override
{
unreachable();
}
};

/**
* Initialize a `Value` from a resolved fetcher attribute.
*/
static void resolvedAttrToValue(EvalState & state, Value & v, const fetchers::ResolvedAttr & resolved)
{
std::visit(
overloaded{
[&](const std::string & s) { v.mkString(s, state.mem); },
[&](uint64_t n) { v.mkInt(n); },
[&](const Explicit<bool> & b) { v.mkBool(b.t); },
},
resolved);
}

/**
* internal primop: Force a LazyFetcherAttr external value.
*/
static void prim_forceLazyFetcherAttr(EvalState & state, const PosIdx pos, Value ** args, Value & v)
{
Value & arg = *args[0];

state.forceValue(arg, pos);
// We only construct this primop with LazyFetcherAttr preapplied.
assert(arg.type() == nExternal);
auto * ext = dynamic_cast<LazyFetcherAttr *>(args[0]->external());
assert(ext);

resolvedAttrToValue(state, v, ext->force());
}

/**
* Emit a lazy thunk for a LazyAttr: mkApp(primop, externalValue).
*/
static void emitLazyAttrThunk(EvalState & state, const fetchers::LazyAttr & lazyAttr, Value & dest)
{
// not user-callable (unregistered, internal)
static PrimOp forcePrimOp{
.name = "__forceLazyFetcherAttr",
.arity = 1,
.impl = prim_forceLazyFetcherAttr,
.internal = true,
};

auto * vExt = state.allocValue();
vExt->mkExternal(new LazyFetcherAttr(lazyAttr));
Comment thread
roberth marked this conversation as resolved.

auto * vPrimOp = state.allocValue();
vPrimOp->mkPrimOp(&forcePrimOp);

dest.mkApp(vPrimOp, vExt);
}

void emitTreeAttrs(
EvalState & state,
const StorePath & storePath,
Expand Down Expand Up @@ -51,7 +142,9 @@ void emitTreeAttrs(
attrs.alloc("shortRev").mkString(emptyHash.gitShortRev(), state.mem);
}

if (auto revCount = input.getRevCount())
if (auto revCount = maybeGetLazyAttr(input.attrs, "revCount"))
emitLazyAttrThunk(state, *revCount, attrs.alloc("revCount"));
else if (auto revCount = input.getRevCount())
attrs.alloc("revCount").mkInt(*revCount);
else if (emptyRevFallback)
attrs.alloc("revCount").mkInt(0);
Expand Down
75 changes: 75 additions & 0 deletions src/libfetchers-tests/attrs.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#include <gtest/gtest.h>

#include "nix/fetchers/attrs.hh"

#include <nlohmann/json.hpp>

namespace nix::fetchers {

TEST(LazyAttr, resolveToInt)
{
Attrs attrs;
attrs.insert_or_assign(
"count", LazyAttr(make_ref<LazyAttrComputation>(LazyAttrComputation{.compute = []() -> ResolvedAttr {
return uint64_t(42);
}})));
EXPECT_EQ(maybeGetIntAttr(attrs, "count"), 42);
}

TEST(LazyAttr, resolveToString)
{
Attrs attrs;
attrs.insert_or_assign(
"name", LazyAttr(make_ref<LazyAttrComputation>(LazyAttrComputation{.compute = []() -> ResolvedAttr {
return std::string("hello");
}})));
EXPECT_EQ(maybeGetStrAttr(attrs, "name"), "hello");
}

TEST(LazyAttr, resolveToBool)
{
Attrs attrs;
attrs.insert_or_assign(
"flag", LazyAttr(make_ref<LazyAttrComputation>(LazyAttrComputation{.compute = []() -> ResolvedAttr {
return Explicit<bool>{true};
}})));
EXPECT_EQ(maybeGetBoolAttr(attrs, "flag"), true);
}

TEST(LazyAttr, attrsToJSONForcesLazy)
{
Attrs attrs;
attrs.insert_or_assign(
"x", LazyAttr(make_ref<LazyAttrComputation>(LazyAttrComputation{.compute = []() -> ResolvedAttr {
return uint64_t(99);
}})));
auto json = attrsToJSON(attrs);
EXPECT_EQ(json["x"], 99);
}

TEST(LazyAttr, attrsToQueryForcesLazy)
{
Attrs attrs;
attrs.insert_or_assign(
"v", LazyAttr(make_ref<LazyAttrComputation>(LazyAttrComputation{.compute = []() -> ResolvedAttr {
return std::string("val");
}})));
auto query = attrsToQuery(attrs);
EXPECT_EQ(query.at("v"), "val");
}

TEST(LazyAttr, notCalledUntilForced)
{
int calls = 0;
Attrs attrs;
attrs.insert_or_assign(
"lazy", LazyAttr(make_ref<LazyAttrComputation>(LazyAttrComputation{.compute = [&calls]() -> ResolvedAttr {
calls++;
return uint64_t(1);
}})));
EXPECT_EQ(calls, 0);
maybeGetIntAttr(attrs, "lazy");
EXPECT_EQ(calls, 1);
}

} // namespace nix::fetchers
1 change: 1 addition & 0 deletions src/libfetchers-tests/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ subdir('nix-meson-build-support/common')

sources = files(
'access-tokens.cc',
'attrs.cc',
'git-utils.cc',
'git.cc',
'input.cc',
Expand Down
Loading
Loading