Merge pull request #762 from ctheune/ctheune-floats

Implement floats
This commit is contained in:
Eelco Dolstra 2016-02-12 12:49:59 +01:00
commit b3e8d72770
27 changed files with 231 additions and 37 deletions

View file

@ -32,7 +32,7 @@ available as <function>builtins.derivation</function>.</para>
<varlistentry><term><function>builtins.add</function>
<replaceable>e1</replaceable> <replaceable>e2</replaceable></term>
<listitem><para>Return the sum of the integers
<listitem><para>Return the sum of the numbers
<replaceable>e1</replaceable> and
<replaceable>e2</replaceable>.</para></listitem>
@ -204,7 +204,7 @@ if builtins ? getEnv then builtins.getEnv "PATH" else ""</programlisting>
<varlistentry><term><function>builtins.div</function>
<replaceable>e1</replaceable> <replaceable>e2</replaceable></term>
<listitem><para>Return the quotient of the integers
<listitem><para>Return the quotient of the numbers
<replaceable>e1</replaceable> and
<replaceable>e2</replaceable>.</para></listitem>
@ -620,12 +620,12 @@ x: x + 456</programlisting>
<varlistentry><term><function>builtins.lessThan</function>
<replaceable>e1</replaceable> <replaceable>e2</replaceable></term>
<listitem><para>Return <literal>true</literal> if the integer
<replaceable>e1</replaceable> is less than the integer
<listitem><para>Return <literal>true</literal> if the number
<replaceable>e1</replaceable> is less than the number
<replaceable>e2</replaceable>, and <literal>false</literal>
otherwise. Evaluation aborts if either
<replaceable>e1</replaceable> or <replaceable>e2</replaceable>
does not evaluate to an integer.</para></listitem>
does not evaluate to a number.</para></listitem>
</varlistentry>
@ -676,7 +676,7 @@ map (x: "foo" + x) [ "bar" "bla" "abc" ]</programlisting>
<varlistentry><term><function>builtins.mul</function>
<replaceable>e1</replaceable> <replaceable>e2</replaceable></term>
<listitem><para>Return the product of the integers
<listitem><para>Return the product of the numbers
<replaceable>e1</replaceable> and
<replaceable>e2</replaceable>.</para></listitem>
@ -833,7 +833,7 @@ builtins.sort builtins.lessThan [ 483 249 526 147 42 77 ]
<varlistentry><term><function>builtins.sub</function>
<replaceable>e1</replaceable> <replaceable>e2</replaceable></term>
<listitem><para>Return the difference between the integers
<listitem><para>Return the difference between the numbers
<replaceable>e1</replaceable> and
<replaceable>e2</replaceable>.</para></listitem>
@ -960,7 +960,7 @@ in foo</programlisting>
<varlistentry><term><function>builtins.toJSON</function> <replaceable>e</replaceable></term>
<listitem><para>Return a string containing a JSON representation
of <replaceable>e</replaceable>. Strings, integers, booleans,
of <replaceable>e</replaceable>. Strings, integers, floats, booleans,
nulls and lists are mapped to their JSON equivalents. Sets
(except derivations) are represented as objects. Derivations are
translated to a JSON string containing the derivations output

View file

@ -43,7 +43,7 @@ of which specify the inputs of the build.</para>
<itemizedlist>
<listitem><para>Strings and integers are just passed
<listitem><para>Strings and numbers are just passed
verbatim.</para></listitem>
<listitem><para>A <emphasis>path</emphasis> (e.g.,

View file

@ -140,8 +140,13 @@ stdenv.mkDerivation {
</listitem>
<listitem><para><emphasis>Integers</emphasis>, e.g.,
<literal>123</literal>.</para></listitem>
<listitem><para>Numbers, which can be <emphasis>integers</emphasis> (like
<literal>123</literal>) or <emphasis>floating point</emphasis> (like
<literal>123.43</literal> or <literal>.27e13</literal>).</para>
<para>Numbers are type-compatible: pure integer operations will always
return integers, whereas any operation involving at least one floating point
number will have a floating point number as a result.</para></listitem>
<listitem><para><emphasis>Paths</emphasis>, e.g.,
<filename>/bin/sh</filename> or <filename>./builder.sh</filename>.

View file

@ -121,6 +121,13 @@ $ diffoscope /nix/store/11a27shh6n2i…-zlib-1.2.8 /nix/store/11a27shh6n2i…-zl
also improves performance.</para>
</listitem>
<listitem>
<para>The Nix language now supports floating point numbers. They are
based on regular C++ <literal>float</literal> and compatible with
existing integers and number-related operations. Export and import to and
from JSON and XML works, too.
</para>
</listitem>
<listitem>
<para>All "chroot"-containing strings got renamed to "sandbox".
In particular, some Nix options got renamed, but the old names

View file

@ -128,6 +128,9 @@ static void printValue(std::ostream & str, std::set<const Value *> & active, con
case tExternal:
str << *v.external;
break;
case tFloat:
str << v.fpoint;
break;
default:
throw Error("invalid value");
}
@ -161,6 +164,7 @@ string showType(const Value & v)
case tPrimOp: return "a built-in function";
case tPrimOpApp: return "a partially applied built-in function";
case tExternal: return v.external->showType();
case tFloat: return "a float";
}
abort();
}
@ -579,6 +583,12 @@ Value * ExprInt::maybeThunk(EvalState & state, Env & env)
return &v;
}
Value * ExprFloat::maybeThunk(EvalState & state, Env & env)
{
nrAvoided++;
return &v;
}
Value * ExprPath::maybeThunk(EvalState & state, Env & env)
{
nrAvoided++;
@ -666,6 +676,11 @@ void ExprInt::eval(EvalState & state, Env & env, Value & v)
}
void ExprFloat::eval(EvalState & state, Env & env, Value & v)
{
v = this->v;
}
void ExprString::eval(EvalState & state, Env & env, Value & v)
{
v = this->v;
@ -1211,6 +1226,7 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
PathSet context;
std::ostringstream s;
NixInt n = 0;
NixFloat nf = 0;
bool first = !forceString;
ValueType firstType = tString;
@ -1229,15 +1245,30 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
}
if (firstType == tInt) {
if (vTmp.type != tInt)
if (vTmp.type == tInt) {
n += vTmp.integer;
} else if (vTmp.type == tFloat) {
// Upgrade the type from int to float;
firstType = tFloat;
nf = n;
nf += vTmp.fpoint;
} else
throwEvalError("cannot add %1% to an integer, at %2%", showType(vTmp), pos);
n += vTmp.integer;
} else if (firstType == tFloat) {
if (vTmp.type == tInt) {
nf += vTmp.integer;
} else if (vTmp.type == tFloat) {
nf += vTmp.fpoint;
} else
throwEvalError("cannot add %1% to a float, at %2%", showType(vTmp), pos);
} else
s << state.coerceToString(pos, vTmp, context, false, firstType == tString);
}
if (firstType == tInt)
mkInt(v, n);
else if (firstType == tFloat)
mkFloat(v, nf);
else if (firstType == tPath) {
if (!context.empty())
throwEvalError("a string that refers to a store path cannot be appended to a path, at %1%", pos);
@ -1295,6 +1326,17 @@ NixInt EvalState::forceInt(Value & v, const Pos & pos)
}
NixFloat EvalState::forceFloat(Value & v, const Pos & pos)
{
forceValue(v, pos);
if (v.type == tInt)
return v.integer;
else if (v.type != tFloat)
throwTypeError("value is %1% while a float was expected, at %2%", v, pos);
return v.fpoint;
}
bool EvalState::forceBool(Value & v)
{
forceValue(v);
@ -1413,6 +1455,7 @@ string EvalState::coerceToString(const Pos & pos, Value & v, PathSet & context,
if (v.type == tBool && v.boolean) return "1";
if (v.type == tBool && !v.boolean) return "";
if (v.type == tInt) return std::to_string(v.integer);
if (v.type == tFloat) return std::to_string(v.fpoint);
if (v.type == tNull) return "";
if (v.isList()) {
@ -1474,6 +1517,13 @@ bool EvalState::eqValues(Value & v1, Value & v2)
uniqList on a list of sets.) Will remove this eventually. */
if (&v1 == &v2) return true;
// Special case type-compatibility between float and int
if (v1.type == tInt && v2.type == tFloat)
return v1.integer == v2.fpoint;
if (v1.type == tFloat && v2.type == tInt)
return v1.fpoint == v2.integer;
// All other types are not compatible with each other.
if (v1.type != v2.type) return false;
switch (v1.type) {
@ -1531,6 +1581,9 @@ bool EvalState::eqValues(Value & v1, Value & v2)
case tExternal:
return *v1.external == *v2.external;
case tFloat:
return v1.fpoint == v2.fpoint;
default:
throwEvalError("cannot compare %1% with %2%", showType(v1), showType(v2));
}

View file

@ -147,6 +147,7 @@ public:
/* Force `v', and then verify that it has the expected type. */
NixInt forceInt(Value & v, const Pos & pos);
NixFloat forceFloat(Value & v, const Pos & pos);
bool forceBool(Value & v);
inline void forceAttrs(Value & v);
inline void forceAttrs(Value & v, const Pos & pos);

View file

@ -106,7 +106,8 @@ bool DrvInfo::checkMeta(Value & v)
if (!checkMeta(*i.value)) return false;
return true;
}
else return v.type == tInt || v.type == tBool || v.type == tString;
else return v.type == tInt || v.type == tBool || v.type == tString ||
v.type == tFloat;
}
@ -127,7 +128,7 @@ string DrvInfo::queryMetaString(const string & name)
}
int DrvInfo::queryMetaInt(const string & name, int def)
NixInt DrvInfo::queryMetaInt(const string & name, NixInt def)
{
Value * v = queryMeta(name);
if (!v) return def;
@ -135,12 +136,26 @@ int DrvInfo::queryMetaInt(const string & name, int def)
if (v->type == tString) {
/* Backwards compatibility with before we had support for
integer meta fields. */
int n;
NixInt n;
if (string2Int(v->string.s, n)) return n;
}
return def;
}
NixFloat DrvInfo::queryMetaFloat(const string & name, NixFloat def)
{
Value * v = queryMeta(name);
if (!v) return def;
if (v->type == tFloat) return v->fpoint;
if (v->type == tString) {
/* Backwards compatibility with before we had support for
float meta fields. */
NixFloat n;
if (string2Float(v->string.s, n)) return n;
}
return def;
}
bool DrvInfo::queryMetaBool(const string & name, bool def)
{

View file

@ -47,7 +47,8 @@ public:
StringSet queryMetaNames();
Value * queryMeta(const string & name);
string queryMetaString(const string & name);
int queryMetaInt(const string & name, int def);
NixInt queryMetaInt(const string & name, NixInt def);
NixFloat queryMetaFloat(const string & name, NixFloat def);
bool queryMetaBool(const string & name, bool def);
void setMeta(const string & name, Value * v);

View file

@ -105,17 +105,22 @@ static void parseJSON(EvalState & state, const char * & s, Value & v)
mkString(v, parseJSONString(s));
}
else if (isdigit(*s) || *s == '-') {
bool neg = false;
if (*s == '-') {
neg = true;
if (!*++s) throw JSONParseError("unexpected end of JSON number");
else if (isdigit(*s) || *s == '-' || *s == '.' ) {
// Buffer into a string first, then use built-in C++ conversions
std::string tmp_number;
ValueType number_type = tInt;
while (isdigit(*s) || *s == '-' || *s == '.' || *s == 'e' || *s == 'E') {
if (*s == '.' || *s == 'e' || *s == 'E')
number_type = tFloat;
tmp_number.append(*s++, 1);
}
if (number_type == tFloat) {
mkFloat(v, stod(tmp_number));
} else {
mkInt(v, stoi(tmp_number));
}
NixInt n = 0;
// FIXME: detect overflow
while (isdigit(*s)) n = n * 10 + (*s++ - '0');
if (*s == '.' || *s == 'e') throw JSONParseError("floating point JSON numbers are not supported");
mkInt(v, neg ? -n : n);
}
else if (strncmp(s, "true", 4) == 0) {

View file

@ -86,6 +86,7 @@ static Expr * unescapeStr(SymbolTable & symbols, const char * s)
ID [a-zA-Z\_][a-zA-Z0-9\_\'\-]*
INT [0-9]+
FLOAT (([1-9][0-9]*\.[0-9]*)|(0?\.[0-9]+))([Ee][+-]?[0-9]+)?
PATH [a-zA-Z0-9\.\_\-\+]*(\/[a-zA-Z0-9\.\_\-\+]+)+
HPATH \~(\/[a-zA-Z0-9\.\_\-\+]+)+
SPATH \<[a-zA-Z0-9\.\_\-\+]+(\/[a-zA-Z0-9\.\_\-\+]+)*\>
@ -126,6 +127,12 @@ or { return OR_KW; }
throw ParseError(format("invalid integer %1%") % yytext);
return INT;
}
{FLOAT} { errno = 0;
yylval->nf = strtod(yytext, 0);
if (errno != 0)
throw ParseError(format("invalid float %1%") % yytext);
return FLOAT;
}
\$\{ { PUSH_STATE(INSIDE_DOLLAR_CURLY); return DOLLAR_CURLY; }
}

View file

@ -68,6 +68,11 @@ void ExprInt::show(std::ostream & str)
str << n;
}
void ExprFloat::show(std::ostream & str)
{
str << nf;
}
void ExprString::show(std::ostream & str)
{
showString(str, s);
@ -226,6 +231,10 @@ void ExprInt::bindVars(const StaticEnv & env)
{
}
void ExprFloat::bindVars(const StaticEnv & env)
{
}
void ExprString::bindVars(const StaticEnv & env)
{
}

View file

@ -98,6 +98,15 @@ struct ExprInt : Expr
Value * maybeThunk(EvalState & state, Env & env);
};
struct ExprFloat : Expr
{
NixFloat nf;
Value v;
ExprFloat(NixFloat nf) : nf(nf) { mkFloat(v, nf); };
COMMON_METHODS
Value * maybeThunk(EvalState & state, Env & env);
};
struct ExprString : Expr
{
Symbol s;

View file

@ -244,6 +244,7 @@ void yyerror(YYLTYPE * loc, yyscan_t scanner, ParseData * data, const char * err
nix::Formals * formals;
nix::Formal * formal;
nix::NixInt n;
nix::NixFloat nf;
const char * id; // !!! -> Symbol
char * path;
char * uri;
@ -264,6 +265,7 @@ void yyerror(YYLTYPE * loc, yyscan_t scanner, ParseData * data, const char * err
%token <id> ID ATTRPATH
%token <e> STR IND_STR
%token <n> INT
%token <nf> FLOAT
%token <path> PATH HPATH SPATH
%token <uri> URI
%token IF THEN ELSE ASSERT WITH LET IN REC INHERIT EQ NEQ AND OR IMPL OR_KW
@ -366,6 +368,7 @@ expr_simple
$$ = new ExprVar(CUR_POS, data->symbols.create($1));
}
| INT { $$ = new ExprInt($1); }
| FLOAT { $$ = new ExprFloat($1); }
| '"' string_parts '"' { $$ = $2; }
| IND_STRING_OPEN ind_string_parts IND_STRING_CLOSE {
$$ = stripIndentation(CUR_POS, data->symbols, *$2);

View file

@ -195,6 +195,7 @@ static void prim_typeOf(EvalState & state, const Pos & pos, Value * * args, Valu
case tExternal:
t = args[0]->external->typeOf();
break;
case tFloat: t = "float"; break;
default: abort();
}
mkString(v, state.symbols.create(t));
@ -224,6 +225,12 @@ static void prim_isInt(EvalState & state, const Pos & pos, Value * * args, Value
mkBool(v, args[0]->type == tInt);
}
/* Determine whether the argument is a float. */
static void prim_isFloat(EvalState & state, const Pos & pos, Value * * args, Value & v)
{
state.forceValue(*args[0]);
mkBool(v, args[0]->type == tFloat);
}
/* Determine whether the argument is a string. */
static void prim_isString(EvalState & state, const Pos & pos, Value * * args, Value & v)
@ -245,11 +252,17 @@ struct CompareValues
{
bool operator () (const Value * v1, const Value * v2) const
{
if (v1->type == tFloat && v2->type == tInt)
return v1->fpoint < v2->integer;
if (v1->type == tInt && v2->type == tFloat)
return v1->integer < v2->fpoint;
if (v1->type != v2->type)
throw EvalError(format("cannot compare %1% with %2%") % showType(*v1) % showType(*v2));
switch (v1->type) {
case tInt:
return v1->integer < v2->integer;
case tFloat:
return v1->fpoint < v2->fpoint;
case tString:
return strcmp(v1->string.s, v2->string.s) < 0;
case tPath:
@ -1423,27 +1436,40 @@ static void prim_sort(EvalState & state, const Pos & pos, Value * * args, Value
static void prim_add(EvalState & state, const Pos & pos, Value * * args, Value & v)
{
mkInt(v, state.forceInt(*args[0], pos) + state.forceInt(*args[1], pos));
if (args[0]->type == tFloat || args[1]->type == tFloat)
mkFloat(v, state.forceFloat(*args[0], pos) + state.forceFloat(*args[1], pos));
else
mkInt(v, state.forceInt(*args[0], pos) + state.forceInt(*args[1], pos));
}
static void prim_sub(EvalState & state, const Pos & pos, Value * * args, Value & v)
{
mkInt(v, state.forceInt(*args[0], pos) - state.forceInt(*args[1], pos));
if (args[0]->type == tFloat || args[1]->type == tFloat)
mkFloat(v, state.forceFloat(*args[0], pos) - state.forceFloat(*args[1], pos));
else
mkInt(v, state.forceInt(*args[0], pos) - state.forceInt(*args[1], pos));
}
static void prim_mul(EvalState & state, const Pos & pos, Value * * args, Value & v)
{
mkInt(v, state.forceInt(*args[0], pos) * state.forceInt(*args[1], pos));
if (args[0]->type == tFloat || args[1]->type == tFloat)
mkFloat(v, state.forceFloat(*args[0], pos) * state.forceFloat(*args[1], pos));
else
mkInt(v, state.forceInt(*args[0], pos) * state.forceInt(*args[1], pos));
}
static void prim_div(EvalState & state, const Pos & pos, Value * * args, Value & v)
{
NixInt i2 = state.forceInt(*args[1], pos);
if (i2 == 0) throw EvalError(format("division by zero, at %1%") % pos);
mkInt(v, state.forceInt(*args[0], pos) / i2);
NixFloat f2 = state.forceFloat(*args[1], pos);
if (f2 == 0) throw EvalError(format("division by zero, at %1%") % pos);
if (args[0]->type == tFloat || args[1]->type == tFloat)
mkFloat(v, state.forceFloat(*args[0], pos) / state.forceFloat(*args[1], pos));
else
mkInt(v, state.forceInt(*args[0], pos) / state.forceInt(*args[1], pos));
}
@ -1735,7 +1761,7 @@ void EvalState::createBaseEnv()
language feature gets added. It's not necessary to increase it
when primops get added, because you can just use `builtins ?
primOp' to check. */
mkInt(v, 3);
mkInt(v, 4);
addConstant("__langVersion", v);
// Miscellaneous
@ -1752,6 +1778,7 @@ void EvalState::createBaseEnv()
addPrimOp("__isFunction", 1, prim_isFunction);
addPrimOp("__isString", 1, prim_isString);
addPrimOp("__isInt", 1, prim_isInt);
addPrimOp("__isFloat", 1, prim_isFloat);
addPrimOp("__isBool", 1, prim_isBool);
addPrimOp("__genericClosure", 1, prim_genericClosure);
addPrimOp("abort", 1, prim_abort);

View file

@ -84,6 +84,10 @@ void printValueAsJSON(EvalState & state, bool strict,
v.external->printValueAsJSON(state, strict, str, context);
break;
case tFloat:
str << v.fpoint;
break;
default:
throw TypeError(format("cannot convert %1% to JSON") % showType(v));
}

View file

@ -148,6 +148,10 @@ static void printValueAsXML(EvalState & state, bool strict, bool location,
v.external->printValueAsXML(state, strict, location, doc, context, drvsSeen);
break;
case tFloat:
doc.writeEmptyElement("float", singletonAttrs("value", (format("%1%") % v.fpoint).str()));
break;
default:
doc.writeEmptyElement("unevaluated");
}

View file

@ -22,6 +22,7 @@ typedef enum {
tPrimOp,
tPrimOpApp,
tExternal,
tFloat
} ValueType;
@ -38,6 +39,7 @@ class XMLWriter;
typedef long NixInt;
typedef float NixFloat;
/* External values must descend from ExternalValueBase, so that
* type-agnostic nix functions (e.g. showType) can be implemented
@ -141,6 +143,7 @@ struct Value
Value * left, * right;
} primOpApp;
ExternalValueBase * external;
NixFloat fpoint;
};
bool isList() const
@ -181,6 +184,14 @@ static inline void mkInt(Value & v, NixInt n)
}
static inline void mkFloat(Value & v, NixFloat n)
{
clearValue(v);
v.type = tFloat;
v.fpoint = n;
}
static inline void mkBool(Value & v, bool b)
{
clearValue(v);

View file

@ -66,6 +66,7 @@ template<class N> N getIntArg(const string & opt,
return n * multiplier;
}
/* Show the manual page for the specified program. */
void showManPage(const string & name);

View file

@ -366,6 +366,14 @@ template<class N> bool string2Int(const string & s, N & n)
return str && str.get() == EOF;
}
/* Parse a string into a float. */
template<class N> bool string2Float(const string & s, N & n)
{
std::istringstream str(s);
str >> n;
return str && str.get() == EOF;
}
/* Return true iff `s' ends in `suffix'. */
bool hasSuffix(const string & s, const string & suffix);

View file

@ -1127,6 +1127,10 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs)
attrs2["type"] = "int";
attrs2["value"] = (format("%1%") % v->integer).str();
xml.writeEmptyElement("meta", attrs2);
} else if (v->type == tFloat) {
attrs2["type"] = "float";
attrs2["value"] = (format("%1%") % v->fpoint).str();
xml.writeEmptyElement("meta", attrs2);
} else if (v->type == tBool) {
attrs2["type"] = "bool";
attrs2["value"] = v->boolean ? "true" : "false";

View file

@ -12,7 +12,9 @@ builtins.fromJSON
"Width": 100
},
"Animated" : false,
"IDs": [116, 943, 234, 38793, true ,false,null, -100]
"IDs": [116, 943, 234, 38793, true ,false,null, -100],
"Latitude": 37.7668,
"Longitude": -122.3959,
}
}
''
@ -28,5 +30,7 @@ builtins.fromJSON
};
Animated = false;
IDs = [ 116 943 234 38793 true false null (0-100) ];
Latitude = 37.7668;
Longitude = -122.3959;
};
}

View file

@ -1 +1 @@
"{\"a\":123,\"b\":-456,\"c\":\"foo\",\"d\":\"foo\\n\\\"bar\\\"\",\"e\":true,\"f\":false,\"g\":[1,2,3],\"h\":[\"a\",[\"b\",{\"foo\\nbar\":{}}]],\"i\":3}"
"{\"a\":123,\"b\":-456,\"c\":\"foo\",\"d\":\"foo\\n\\\"bar\\\"\",\"e\":true,\"f\":false,\"g\":[1,2,3],\"h\":[\"a\",[\"b\",{\"foo\\nbar\":{}}]],\"i\":3,\"j\":1.44}"

View file

@ -8,4 +8,5 @@ builtins.toJSON
g = [ 1 2 3 ];
h = [ "a" [ "b" { "foo\nbar" = {}; } ] ];
i = 1 + 2;
j = 1.44;
}

View file

@ -1 +1 @@
[ true false true false true false true false true false true false "int" "bool" "string" "null" "set" "list" "lambda" "lambda" "lambda" "lambda" ]
[ true false true false true false true false true true true true true true true true true true true false true false "int" "bool" "string" "null" "set" "list" "lambda" "lambda" "lambda" "lambda" ]

View file

@ -8,6 +8,16 @@ with builtins;
(isString [ "x" ])
(isInt (1 + 2))
(isInt { x = 123; })
(isInt (1 / 2))
(isInt (1 + 1))
(isInt (1 / 2))
(isInt (1 * 2))
(isInt (1 - 2))
(isFloat (1.2))
(isFloat (1 + 1.0))
(isFloat (1 / 2.0))
(isFloat (1 * 2.0))
(isFloat (1 - 2.0))
(isBool (true && false))
(isBool null)
(isAttrs { x = 123; })

View file

@ -45,5 +45,8 @@
<attr name="x">
<int value="123" />
</attr>
<attr name="y">
<float value="567.89" />
</attr>
</attrs>
</expr>

View file

@ -2,6 +2,8 @@ rec {
x = 123;
y = 567.890;
a = "foo";
b = "bar";