Skip to content

Commit e6afa20

Browse files
committed
Encapsulate and slightly optimize string contexts
These steps are done (originally in order, but I squashed it as the end result is still pretty small, and the churn in the code comments was a bit annoying to keep straight). 1. Create proper struct type for string contexts on the heap This will make it easier to change this type in the future. 2. Make `Value::StringWithContext` iterable This make some for loops a lot more terse. 3. Encapsulate `Value::StringWithContext::Context::elems` It turns out the iterators we just exposed are sufficient. 4. Make `StringWithContext::Context` length-prefixed instead Rather than having a null pointer at the end, have a `size_t` at the beginning. This is the exact same size (note that null pointer is longer than null byte) and thus takes no more space! Also, see the new TODO on naming. The thing we already so-named is a builder type for string contexts, not the on-heap type. The `fromBuilder` static method reflects what the names ought to be too.
1 parent 5b15544 commit e6afa20

File tree

4 files changed

+87
-19
lines changed

4 files changed

+87
-19
lines changed

src/libexpr/eval-cache.cc

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,17 +136,19 @@ struct AttrDb
136136
});
137137
}
138138

139-
AttrId setString(AttrKey key, std::string_view s, const char ** context = nullptr)
139+
AttrId setString(AttrKey key, std::string_view s, const Value::StringWithContext::Context * context = nullptr)
140140
{
141141
return doSQLite([&]() {
142142
auto state(_state->lock());
143143

144144
if (context) {
145145
std::string ctx;
146-
for (const char ** p = context; *p; ++p) {
147-
if (p != context)
146+
bool first = true;
147+
for (auto * elem : *context) {
148+
if (!first)
148149
ctx.push_back(' ');
149-
ctx.append(*p);
150+
ctx.append(elem);
151+
first = false;
150152
}
151153
state->insertAttributeWithContext.use()(key.first)(symbols[key.second])(AttrType::String) (s) (ctx)
152154
.exec();

src/libexpr/eval.cc

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -821,28 +821,31 @@ void Value::mkString(std::string_view s)
821821
mkStringNoCopy(makeImmutableString(s));
822822
}
823823

824-
static const char ** encodeContext(const NixStringContext & context)
824+
Value::StringWithContext::Context * Value::StringWithContext::Context::fromBuilder(const NixStringContext & context)
825825
{
826826
if (!context.empty()) {
827-
size_t n = 0;
828-
auto ctx = (const char **) allocBytes((context.size() + 1) * sizeof(char *));
829-
for (auto & i : context) {
830-
ctx[n++] = makeImmutableString({i.to_string()});
827+
auto ctx = (Value::StringWithContext::Context *) allocBytes(sizeof(size_t) + context.size() * sizeof(char *));
828+
ctx->size = context.size();
829+
/* Mapping the original iterator to turn references into
830+
pointers is necessary to make sure that enumerate doesn't
831+
accidently copy the elements when it returns tuples by value.
832+
*/
833+
for (auto [n, i] : enumerate(context | std::views::transform([](const auto & r) { return &r; }))) {
834+
ctx->elems[n] = makeImmutableString({i->to_string()});
831835
}
832-
ctx[n] = nullptr;
833836
return ctx;
834837
} else
835838
return nullptr;
836839
}
837840

838841
void Value::mkString(std::string_view s, const NixStringContext & context)
839842
{
840-
mkStringNoCopy(makeImmutableString(s), encodeContext(context));
843+
mkStringNoCopy(makeImmutableString(s), Value::StringWithContext::Context::fromBuilder(context));
841844
}
842845

843846
void Value::mkStringMove(const char * s, const NixStringContext & context)
844847
{
845-
mkStringNoCopy(s, encodeContext(context));
848+
mkStringNoCopy(s, Value::StringWithContext::Context::fromBuilder(context));
846849
}
847850

848851
void Value::mkPath(const SourcePath & path)
@@ -2287,9 +2290,9 @@ std::string_view EvalState::forceString(Value & v, const PosIdx pos, std::string
22872290

22882291
void copyContext(const Value & v, NixStringContext & context, const ExperimentalFeatureSettings & xpSettings)
22892292
{
2290-
if (v.context())
2291-
for (const char ** p = v.context(); *p; ++p)
2292-
context.insert(NixStringContextElem::parse(*p, xpSettings));
2293+
if (auto * ctx = v.context())
2294+
for (auto * elem : *ctx)
2295+
context.insert(NixStringContextElem::parse(elem, xpSettings));
22932296
}
22942297

22952298
std::string_view EvalState::forceString(
@@ -2309,7 +2312,9 @@ std::string_view EvalState::forceStringNoCtx(Value & v, const PosIdx pos, std::s
23092312
auto s = forceString(v, pos, errorCtx);
23102313
if (v.context()) {
23112314
error<EvalError>(
2312-
"the string '%1%' is not allowed to refer to a store path (such as '%2%')", v.string_view(), v.context()[0])
2315+
"the string '%1%' is not allowed to refer to a store path (such as '%2%')",
2316+
v.string_view(),
2317+
*v.context()->begin())
23132318
.withTrace(pos, errorCtx)
23142319
.debugThrow();
23152320
}

src/libexpr/include/nix/expr/value.hh

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,55 @@ struct ValueBase
220220
struct StringWithContext
221221
{
222222
const char * c_str;
223-
const char ** context; // must be in sorted order
223+
224+
/**
225+
* The type of the context itself.
226+
*
227+
* Currently, it is length-prefixed array of pointers to
228+
* null-terminated strings. The strings are specially formatted
229+
* to represent a flattening of the recursive sum type that is a
230+
* context element.
231+
*
232+
* @See NixStringContext for an more easily understood type,
233+
* that of the "builder" for this data structure.
234+
*/
235+
struct Context
236+
{
237+
using Elem = const char *;
238+
239+
/**
240+
* Number of items in the array
241+
*/
242+
size_t size;
243+
244+
private:
245+
/**
246+
* must be in sorted order
247+
*/
248+
Elem elems[/*size*/];
249+
public:
250+
251+
const Elem * begin() const
252+
{
253+
return elems;
254+
}
255+
256+
const Elem * end() const
257+
{
258+
return &elems[size];
259+
}
260+
261+
/**
262+
* @note returns a null pointer to more concisely encode the
263+
* empty context
264+
*/
265+
static Context * fromBuilder(const NixStringContext & context);
266+
};
267+
268+
/**
269+
* May be null for a string without context.
270+
*/
271+
const Context * context;
224272
};
225273

226274
struct Path
@@ -991,7 +1039,7 @@ public:
9911039
setStorage(b);
9921040
}
9931041

994-
void mkStringNoCopy(const char * s, const char ** context = 0) noexcept
1042+
void mkStringNoCopy(const char * s, const Value::StringWithContext::Context * context = nullptr) noexcept
9951043
{
9961044
setStorage(StringWithContext{.c_str = s, .context = context});
9971045
}
@@ -1117,7 +1165,7 @@ public:
11171165
return getStorage<StringWithContext>().c_str;
11181166
}
11191167

1120-
const char ** context() const noexcept
1168+
const Value::StringWithContext::Context * context() const noexcept
11211169
{
11221170
return getStorage<StringWithContext>().context;
11231171
}

src/libexpr/include/nix/expr/value/context.hh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ public:
2424
}
2525
};
2626

27+
/**
28+
* @todo This should be reamed to `StringContextBuilderElem`, since:
29+
*
30+
* 1. We use `*Builder` for off-heap temporary data structures
31+
*
32+
* 2. The `Nix*` is totally redundant. (And my mistake from a long time
33+
* ago.)
34+
*/
2735
struct NixStringContextElem
2836
{
2937
/**
@@ -77,6 +85,11 @@ struct NixStringContextElem
7785
std::string to_string() const;
7886
};
7987

88+
/**
89+
* @todo This should be renamed to `StringContextBuilder`.
90+
*
91+
* @see NixStringContextElem for explanation why.
92+
*/
8093
typedef std::set<NixStringContextElem> NixStringContext;
8194

8295
} // namespace nix

0 commit comments

Comments
 (0)