2012-07-18 18:59:03 +00:00
|
|
|
#pragma once
|
2012-01-07 17:26:33 +00:00
|
|
|
|
2021-11-24 19:21:34 +00:00
|
|
|
#include <cassert>
|
|
|
|
|
2012-01-07 17:26:33 +00:00
|
|
|
#include "symbol-table.hh"
|
|
|
|
|
2016-08-30 11:12:12 +00:00
|
|
|
#if HAVE_BOEHMGC
|
|
|
|
#include <gc/gc_allocator.h>
|
|
|
|
#endif
|
|
|
|
|
2012-01-07 17:26:33 +00:00
|
|
|
namespace nix {
|
|
|
|
|
2022-01-04 16:39:16 +00:00
|
|
|
class BindingsBuilder;
|
|
|
|
|
2012-01-07 17:26:33 +00:00
|
|
|
|
|
|
|
typedef enum {
|
|
|
|
tInt = 1,
|
|
|
|
tBool,
|
|
|
|
tString,
|
|
|
|
tPath,
|
|
|
|
tNull,
|
|
|
|
tAttrs,
|
2015-07-23 20:05:09 +00:00
|
|
|
tList1,
|
|
|
|
tList2,
|
|
|
|
tListN,
|
2012-01-07 17:26:33 +00:00
|
|
|
tThunk,
|
|
|
|
tApp,
|
|
|
|
tLambda,
|
|
|
|
tBlackhole,
|
|
|
|
tPrimOp,
|
|
|
|
tPrimOpApp,
|
2014-11-30 18:16:19 +00:00
|
|
|
tExternal,
|
2016-01-04 23:40:40 +00:00
|
|
|
tFloat
|
2020-12-17 13:42:52 +00:00
|
|
|
} InternalType;
|
2012-01-07 17:26:33 +00:00
|
|
|
|
2020-12-11 22:32:45 +00:00
|
|
|
// This type abstracts over all actual value types in the language,
|
|
|
|
// grouping together implementation details like tList*, different function
|
|
|
|
// types, and types in non-normal form (so thunks and co.)
|
|
|
|
typedef enum {
|
|
|
|
nThunk,
|
|
|
|
nInt,
|
|
|
|
nFloat,
|
|
|
|
nBool,
|
|
|
|
nString,
|
|
|
|
nPath,
|
|
|
|
nNull,
|
|
|
|
nAttrs,
|
|
|
|
nList,
|
|
|
|
nFunction,
|
|
|
|
nExternal
|
2020-12-17 13:42:52 +00:00
|
|
|
} ValueType;
|
2012-01-07 17:26:33 +00:00
|
|
|
|
2014-01-21 17:29:55 +00:00
|
|
|
class Bindings;
|
2012-01-07 17:26:33 +00:00
|
|
|
struct Env;
|
|
|
|
struct Expr;
|
|
|
|
struct ExprLambda;
|
|
|
|
struct PrimOp;
|
2022-03-05 13:40:24 +00:00
|
|
|
class SymbolIdx;
|
2022-03-04 18:31:59 +00:00
|
|
|
class PosIdx;
|
2014-11-30 18:16:19 +00:00
|
|
|
struct Pos;
|
2022-03-12 00:28:00 +00:00
|
|
|
class StorePath;
|
|
|
|
class Store;
|
2014-11-30 18:16:19 +00:00
|
|
|
class EvalState;
|
|
|
|
class XMLWriter;
|
2016-08-26 16:55:55 +00:00
|
|
|
class JSONPlaceholder;
|
2012-01-07 17:26:33 +00:00
|
|
|
|
|
|
|
|
libexpr: Use int64_t for NixInt
Using a 64bit integer on 32bit systems will come with a bit of a
performance overhead, but given that Nix doesn't use a lot of integers
compared to other types, I think the overhead is negligible also
considering that 32bit systems are in decline.
The biggest advantage however is that when we use a consistent integer
size across all platforms it's less likely that we miss things that we
break due to that. One example would be:
https://github.com/NixOS/nixpkgs/pull/44233
On Hydra it will evaluate, because the evaluator runs on a 64bit
machine, but when evaluating the same on a 32bit machine it will fail,
so using 64bit integers should make that consistent.
While the change of the type in value.hh is rather easy to do, we have a
few more options available for doing the conversion in the lexer:
* Via an #ifdef on the architecture and using strtol() or strtoll()
accordingly depending on which architecture we are. For the #ifdef
we would need another AX_COMPILE_CHECK_SIZEOF in configure.ac.
* Using istringstream, which would involve copying the value.
* As we're already using boost, lexical_cast might be a good idea.
Spoiler: I went for the latter, first of all because lexical_cast does
have an overload for const char* and second of all, because it doesn't
involve copying around the input string. Also, because istringstream
seems to come with a bigger overhead than boost::lexical_cast:
https://www.boost.org/doc/libs/release/doc/html/boost_lexical_cast/performance.html
The first method (still using strtol/strtoll) also wasn't something I
pursued further, because it is also locale-aware which I doubt is what
we want, given that the regex for int is [0-9]+.
Signed-off-by: aszlig <aszlig@nix.build>
Fixes: #2339
2018-08-28 22:23:51 +00:00
|
|
|
typedef int64_t NixInt;
|
2018-07-03 16:04:51 +00:00
|
|
|
typedef double NixFloat;
|
2022-03-12 00:28:00 +00:00
|
|
|
typedef std::pair<StorePath, std::string> NixStringContextElem;
|
2021-03-10 02:20:32 +00:00
|
|
|
typedef std::vector<NixStringContextElem> NixStringContext;
|
2013-08-19 10:35:03 +00:00
|
|
|
|
2014-11-30 18:16:19 +00:00
|
|
|
/* External values must descend from ExternalValueBase, so that
|
|
|
|
* type-agnostic nix functions (e.g. showType) can be implemented
|
|
|
|
*/
|
|
|
|
class ExternalValueBase
|
|
|
|
{
|
2014-12-02 15:02:03 +00:00
|
|
|
friend std::ostream & operator << (std::ostream & str, const ExternalValueBase & v);
|
2014-11-30 18:16:19 +00:00
|
|
|
protected:
|
|
|
|
/* Print out the value */
|
2014-12-02 15:02:03 +00:00
|
|
|
virtual std::ostream & print(std::ostream & str) const = 0;
|
2014-11-30 18:16:19 +00:00
|
|
|
|
|
|
|
public:
|
|
|
|
/* Return a simple string describing the type */
|
2022-02-21 15:37:25 +00:00
|
|
|
virtual std::string showType() const = 0;
|
2014-11-30 18:16:19 +00:00
|
|
|
|
|
|
|
/* Return a string to be used in builtins.typeOf */
|
2022-02-21 15:37:25 +00:00
|
|
|
virtual std::string typeOf() const = 0;
|
2014-11-30 18:16:19 +00:00
|
|
|
|
|
|
|
/* Coerce the value to a string. Defaults to uncoercable, i.e. throws an
|
2022-02-21 15:37:25 +00:00
|
|
|
* error.
|
2014-11-30 18:16:19 +00:00
|
|
|
*/
|
2022-02-21 15:37:25 +00:00
|
|
|
virtual std::string coerceToString(const Pos & pos, PathSet & context, bool copyMore, bool copyToStore) const;
|
2014-11-30 18:16:19 +00:00
|
|
|
|
|
|
|
/* Compare to another value of the same type. Defaults to uncomparable,
|
|
|
|
* i.e. always false.
|
|
|
|
*/
|
2022-02-21 15:37:25 +00:00
|
|
|
virtual bool operator ==(const ExternalValueBase & b) const;
|
2014-11-30 18:16:19 +00:00
|
|
|
|
|
|
|
/* Print the value as JSON. Defaults to unconvertable, i.e. throws an error */
|
|
|
|
virtual void printValueAsJSON(EvalState & state, bool strict,
|
2016-08-26 16:55:55 +00:00
|
|
|
JSONPlaceholder & out, PathSet & context) const;
|
2014-11-30 18:16:19 +00:00
|
|
|
|
|
|
|
/* Print the value as XML. Defaults to unevaluated */
|
|
|
|
virtual void printValueAsXML(EvalState & state, bool strict, bool location,
|
2021-11-14 01:29:31 +00:00
|
|
|
XMLWriter & doc, PathSet & context, PathSet & drvsSeen,
|
2022-03-04 18:31:59 +00:00
|
|
|
const PosIdx pos) const;
|
2014-11-30 18:16:19 +00:00
|
|
|
|
|
|
|
virtual ~ExternalValueBase()
|
|
|
|
{
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2014-12-02 15:02:03 +00:00
|
|
|
std::ostream & operator << (std::ostream & str, const ExternalValueBase & v);
|
2014-11-30 18:16:19 +00:00
|
|
|
|
2013-08-19 10:35:03 +00:00
|
|
|
|
2012-01-07 17:26:33 +00:00
|
|
|
struct Value
|
|
|
|
{
|
2020-12-12 01:22:58 +00:00
|
|
|
private:
|
2020-12-17 13:42:52 +00:00
|
|
|
InternalType internalType;
|
2020-12-11 23:19:05 +00:00
|
|
|
|
2022-03-03 12:07:50 +00:00
|
|
|
friend std::string showType(const Value & v);
|
2022-03-22 12:18:11 +00:00
|
|
|
|
2022-03-05 13:40:24 +00:00
|
|
|
void print(const SymbolTable & symbols, std::ostream & str, std::set<const void *> * seen) const;
|
2020-12-12 01:22:58 +00:00
|
|
|
|
|
|
|
public:
|
|
|
|
|
2022-03-05 13:40:24 +00:00
|
|
|
void print(const SymbolTable & symbols, std::ostream & str, bool showRepeated = false) const;
|
2022-03-22 12:18:11 +00:00
|
|
|
|
2020-12-12 01:15:11 +00:00
|
|
|
// Functions needed to distinguish the type
|
|
|
|
// These should be removed eventually, by putting the functionality that's
|
|
|
|
// needed by callers into methods of this type
|
|
|
|
|
2020-12-17 13:45:45 +00:00
|
|
|
// type() == nThunk
|
2020-12-17 13:42:52 +00:00
|
|
|
inline bool isThunk() const { return internalType == tThunk; };
|
|
|
|
inline bool isApp() const { return internalType == tApp; };
|
|
|
|
inline bool isBlackhole() const { return internalType == tBlackhole; };
|
2020-12-12 01:15:11 +00:00
|
|
|
|
2020-12-17 13:45:45 +00:00
|
|
|
// type() == nFunction
|
2020-12-17 13:42:52 +00:00
|
|
|
inline bool isLambda() const { return internalType == tLambda; };
|
|
|
|
inline bool isPrimOp() const { return internalType == tPrimOp; };
|
|
|
|
inline bool isPrimOpApp() const { return internalType == tPrimOpApp; };
|
2020-12-12 01:15:11 +00:00
|
|
|
|
2013-08-19 10:35:03 +00:00
|
|
|
union
|
2012-01-07 17:26:33 +00:00
|
|
|
{
|
2013-08-19 10:35:03 +00:00
|
|
|
NixInt integer;
|
2012-01-07 17:26:33 +00:00
|
|
|
bool boolean;
|
2013-08-19 10:35:03 +00:00
|
|
|
|
2013-08-06 12:15:11 +00:00
|
|
|
/* Strings in the evaluator carry a so-called `context' which
|
|
|
|
is a list of strings representing store paths. This is to
|
|
|
|
allow users to write things like
|
2012-01-07 17:26:33 +00:00
|
|
|
|
|
|
|
"--with-freetype2-library=" + freetype + "/lib"
|
|
|
|
|
|
|
|
where `freetype' is a derivation (or a source to be copied
|
|
|
|
to the store). If we just concatenated the strings without
|
|
|
|
keeping track of the referenced store paths, then if the
|
|
|
|
string is used as a derivation attribute, the derivation
|
|
|
|
will not have the correct dependencies in its inputDrvs and
|
|
|
|
inputSrcs.
|
|
|
|
|
|
|
|
The semantics of the context is as follows: when a string
|
|
|
|
with context C is used as a derivation attribute, then the
|
|
|
|
derivations in C will be added to the inputDrvs of the
|
|
|
|
derivation, and the other store paths in C will be added to
|
|
|
|
the inputSrcs of the derivations.
|
|
|
|
|
|
|
|
For canonicity, the store paths should be in sorted order. */
|
|
|
|
struct {
|
|
|
|
const char * s;
|
|
|
|
const char * * context; // must be in sorted order
|
|
|
|
} string;
|
2013-08-19 10:35:03 +00:00
|
|
|
|
2012-01-07 17:26:33 +00:00
|
|
|
const char * path;
|
|
|
|
Bindings * attrs;
|
|
|
|
struct {
|
2018-05-02 11:56:34 +00:00
|
|
|
size_t size;
|
2012-01-07 17:26:33 +00:00
|
|
|
Value * * elems;
|
2015-07-23 20:05:09 +00:00
|
|
|
} bigList;
|
|
|
|
Value * smallList[2];
|
2012-01-07 17:26:33 +00:00
|
|
|
struct {
|
|
|
|
Env * env;
|
|
|
|
Expr * expr;
|
|
|
|
} thunk;
|
|
|
|
struct {
|
|
|
|
Value * left, * right;
|
|
|
|
} app;
|
|
|
|
struct {
|
|
|
|
Env * env;
|
|
|
|
ExprLambda * fun;
|
|
|
|
} lambda;
|
|
|
|
PrimOp * primOp;
|
|
|
|
struct {
|
|
|
|
Value * left, * right;
|
|
|
|
} primOpApp;
|
2014-11-30 18:16:19 +00:00
|
|
|
ExternalValueBase * external;
|
2016-01-04 23:40:40 +00:00
|
|
|
NixFloat fpoint;
|
2012-01-07 17:26:33 +00:00
|
|
|
};
|
2015-07-23 20:05:09 +00:00
|
|
|
|
2020-12-11 22:32:45 +00:00
|
|
|
// Returns the normal type of a Value. This only returns nThunk if the
|
|
|
|
// Value hasn't been forceValue'd
|
2020-12-17 13:45:45 +00:00
|
|
|
inline ValueType type() const
|
2020-12-11 22:32:45 +00:00
|
|
|
{
|
2020-12-17 13:42:52 +00:00
|
|
|
switch (internalType) {
|
2020-12-11 22:32:45 +00:00
|
|
|
case tInt: return nInt;
|
|
|
|
case tBool: return nBool;
|
|
|
|
case tString: return nString;
|
|
|
|
case tPath: return nPath;
|
|
|
|
case tNull: return nNull;
|
|
|
|
case tAttrs: return nAttrs;
|
|
|
|
case tList1: case tList2: case tListN: return nList;
|
|
|
|
case tLambda: case tPrimOp: case tPrimOpApp: return nFunction;
|
|
|
|
case tExternal: return nExternal;
|
|
|
|
case tFloat: return nFloat;
|
|
|
|
case tThunk: case tApp: case tBlackhole: return nThunk;
|
|
|
|
}
|
|
|
|
abort();
|
|
|
|
}
|
|
|
|
|
2020-12-18 13:38:49 +00:00
|
|
|
/* After overwriting an app node, be sure to clear pointers in the
|
|
|
|
Value to ensure that the target isn't kept alive unnecessarily. */
|
|
|
|
inline void clearValue()
|
|
|
|
{
|
|
|
|
app.left = app.right = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline void mkInt(NixInt n)
|
|
|
|
{
|
|
|
|
clearValue();
|
|
|
|
internalType = tInt;
|
|
|
|
integer = n;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline void mkBool(bool b)
|
|
|
|
{
|
|
|
|
clearValue();
|
|
|
|
internalType = tBool;
|
|
|
|
boolean = b;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline void mkString(const char * s, const char * * context = 0)
|
|
|
|
{
|
|
|
|
internalType = tString;
|
|
|
|
string.s = s;
|
|
|
|
string.context = context;
|
|
|
|
}
|
|
|
|
|
2022-01-04 16:39:16 +00:00
|
|
|
void mkString(std::string_view s);
|
|
|
|
|
|
|
|
void mkString(std::string_view s, const PathSet & context);
|
|
|
|
|
2021-12-27 01:04:49 +00:00
|
|
|
void mkStringMove(const char * s, const PathSet & context);
|
|
|
|
|
2022-01-04 18:09:40 +00:00
|
|
|
inline void mkString(const Symbol & s)
|
|
|
|
{
|
2022-03-05 13:40:24 +00:00
|
|
|
mkString(std::string_view(s).data());
|
2022-01-04 18:09:40 +00:00
|
|
|
}
|
|
|
|
|
2020-12-18 13:38:49 +00:00
|
|
|
inline void mkPath(const char * s)
|
|
|
|
{
|
|
|
|
clearValue();
|
|
|
|
internalType = tPath;
|
|
|
|
path = s;
|
|
|
|
}
|
|
|
|
|
2022-01-04 17:45:16 +00:00
|
|
|
void mkPath(std::string_view s);
|
|
|
|
|
2020-12-18 13:38:49 +00:00
|
|
|
inline void mkNull()
|
|
|
|
{
|
|
|
|
clearValue();
|
|
|
|
internalType = tNull;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline void mkAttrs(Bindings * a)
|
|
|
|
{
|
|
|
|
clearValue();
|
|
|
|
internalType = tAttrs;
|
|
|
|
attrs = a;
|
|
|
|
}
|
|
|
|
|
2022-01-04 16:39:16 +00:00
|
|
|
Value & mkAttrs(BindingsBuilder & bindings);
|
|
|
|
|
2020-12-18 13:38:49 +00:00
|
|
|
inline void mkList(size_t size)
|
|
|
|
{
|
|
|
|
clearValue();
|
|
|
|
if (size == 1)
|
|
|
|
internalType = tList1;
|
|
|
|
else if (size == 2)
|
|
|
|
internalType = tList2;
|
|
|
|
else {
|
|
|
|
internalType = tListN;
|
|
|
|
bigList.size = size;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
inline void mkThunk(Env * e, Expr * ex)
|
|
|
|
{
|
|
|
|
internalType = tThunk;
|
|
|
|
thunk.env = e;
|
|
|
|
thunk.expr = ex;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline void mkApp(Value * l, Value * r)
|
|
|
|
{
|
|
|
|
internalType = tApp;
|
|
|
|
app.left = l;
|
|
|
|
app.right = r;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline void mkLambda(Env * e, ExprLambda * f)
|
|
|
|
{
|
|
|
|
internalType = tLambda;
|
|
|
|
lambda.env = e;
|
|
|
|
lambda.fun = f;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline void mkBlackhole()
|
|
|
|
{
|
|
|
|
internalType = tBlackhole;
|
|
|
|
// Value will be overridden anyways
|
|
|
|
}
|
|
|
|
|
|
|
|
inline void mkPrimOp(PrimOp * p)
|
|
|
|
{
|
|
|
|
clearValue();
|
|
|
|
internalType = tPrimOp;
|
|
|
|
primOp = p;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
inline void mkPrimOpApp(Value * l, Value * r)
|
|
|
|
{
|
|
|
|
internalType = tPrimOpApp;
|
|
|
|
app.left = l;
|
|
|
|
app.right = r;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline void mkExternal(ExternalValueBase * e)
|
|
|
|
{
|
|
|
|
clearValue();
|
|
|
|
internalType = tExternal;
|
|
|
|
external = e;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline void mkFloat(NixFloat n)
|
|
|
|
{
|
|
|
|
clearValue();
|
|
|
|
internalType = tFloat;
|
|
|
|
fpoint = n;
|
|
|
|
}
|
|
|
|
|
2015-07-23 20:05:09 +00:00
|
|
|
bool isList() const
|
|
|
|
{
|
2020-12-17 13:42:52 +00:00
|
|
|
return internalType == tList1 || internalType == tList2 || internalType == tListN;
|
2015-07-23 20:05:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Value * * listElems()
|
|
|
|
{
|
2020-12-17 13:42:52 +00:00
|
|
|
return internalType == tList1 || internalType == tList2 ? smallList : bigList.elems;
|
2015-07-23 20:05:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const Value * const * listElems() const
|
|
|
|
{
|
2020-12-17 13:42:52 +00:00
|
|
|
return internalType == tList1 || internalType == tList2 ? smallList : bigList.elems;
|
2015-07-23 20:05:09 +00:00
|
|
|
}
|
|
|
|
|
2018-05-02 11:56:34 +00:00
|
|
|
size_t listSize() const
|
2015-07-23 20:05:09 +00:00
|
|
|
{
|
2020-12-17 13:42:52 +00:00
|
|
|
return internalType == tList1 ? 1 : internalType == tList2 ? 2 : bigList.size;
|
2015-07-23 20:05:09 +00:00
|
|
|
}
|
2019-09-09 15:34:38 +00:00
|
|
|
|
2022-03-04 18:31:59 +00:00
|
|
|
PosIdx determinePos(const PosIdx pos) const;
|
2021-01-08 21:27:00 +00:00
|
|
|
|
2019-09-09 15:34:38 +00:00
|
|
|
/* Check whether forcing this value requires a trivial amount of
|
|
|
|
computation. In particular, function applications are
|
|
|
|
non-trivial. */
|
|
|
|
bool isTrivial() const;
|
2020-06-29 17:08:37 +00:00
|
|
|
|
2022-03-12 00:28:00 +00:00
|
|
|
NixStringContext getContext(const Store &);
|
2021-11-24 19:21:34 +00:00
|
|
|
|
|
|
|
auto listItems()
|
|
|
|
{
|
|
|
|
struct ListIterable
|
|
|
|
{
|
|
|
|
typedef Value * const * iterator;
|
|
|
|
iterator _begin, _end;
|
|
|
|
iterator begin() const { return _begin; }
|
|
|
|
iterator end() const { return _end; }
|
|
|
|
};
|
|
|
|
assert(isList());
|
|
|
|
auto begin = listElems();
|
|
|
|
return ListIterable { begin, begin + listSize() };
|
|
|
|
}
|
|
|
|
|
|
|
|
auto listItems() const
|
|
|
|
{
|
|
|
|
struct ConstListIterable
|
|
|
|
{
|
|
|
|
typedef const Value * const * iterator;
|
|
|
|
iterator _begin, _end;
|
|
|
|
iterator begin() const { return _begin; }
|
|
|
|
iterator end() const { return _end; }
|
|
|
|
};
|
|
|
|
assert(isList());
|
|
|
|
auto begin = listElems();
|
|
|
|
return ConstListIterable { begin, begin + listSize() };
|
|
|
|
}
|
2012-01-07 17:26:33 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
2016-08-29 15:28:20 +00:00
|
|
|
#if HAVE_BOEHMGC
|
2020-04-16 15:24:28 +00:00
|
|
|
typedef std::vector<Value *, traceable_allocator<Value *> > ValueVector;
|
2022-03-05 13:40:24 +00:00
|
|
|
typedef std::map<SymbolIdx, Value *, std::less<SymbolIdx>, traceable_allocator<std::pair<const SymbolIdx, Value *> > > ValueMap;
|
|
|
|
typedef std::map<SymbolIdx, ValueVector, std::less<SymbolIdx>, traceable_allocator<std::pair<const SymbolIdx, ValueVector> > > ValueVectorMap;
|
2016-08-29 15:28:20 +00:00
|
|
|
#else
|
|
|
|
typedef std::vector<Value *> ValueVector;
|
2022-03-05 13:40:24 +00:00
|
|
|
typedef std::map<SymbolIdx, Value *> ValueMap;
|
|
|
|
typedef std::map<SymbolIdx, ValueVector> ValueVectorMap;
|
2016-08-29 15:28:20 +00:00
|
|
|
#endif
|
|
|
|
|
|
|
|
|
2020-04-16 14:28:07 +00:00
|
|
|
/* A value allocated in traceable memory. */
|
|
|
|
typedef std::shared_ptr<Value *> RootValue;
|
|
|
|
|
|
|
|
RootValue allocRootValue(Value * v);
|
|
|
|
|
2012-01-07 17:26:33 +00:00
|
|
|
}
|