forked from lix-project/lix
* Some syntactic sugar for attribute sets: allow {x.y.z = ...;} as a
shorthand for {x = {y = {z = ...;};};}. This is especially useful for NixOS configuration files, e.g. { services = { sshd = { enable = true; port = 2022; }; }; } can now be written as { services.sshd.enable = true; services.sshd.port = 2022; } However, it is currently not permitted to write { services.sshd = {enable = true;}; services.sshd.port = 2022; } as this is considered a duplicate definition of `services.sshd'.
This commit is contained in:
parent
e42975490f
commit
d407d572fd
|
@ -70,6 +70,7 @@ Bool | ATermBool | Expr |
|
||||||
Null | | Expr |
|
Null | | Expr |
|
||||||
|
|
||||||
Bind | string Expr Pos | ATerm |
|
Bind | string Expr Pos | ATerm |
|
||||||
|
BindAttrPath | ATermList Expr Pos | ATerm | # desugared during parsing
|
||||||
Bind | string Expr | ATerm | ObsoleteBind
|
Bind | string Expr | ATerm | ObsoleteBind
|
||||||
Inherit | Expr ATermList Pos | ATerm |
|
Inherit | Expr ATermList Pos | ATerm |
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ string showPos(ATerm pos)
|
||||||
if (matchNoPos(pos)) return "undefined position";
|
if (matchNoPos(pos)) return "undefined position";
|
||||||
if (!matchPos(pos, path, line, column))
|
if (!matchPos(pos, path, line, column))
|
||||||
throw badTerm("position expected", pos);
|
throw badTerm("position expected", pos);
|
||||||
return (format("`%1%', line %2%") % aterm2String(path) % line).str();
|
return (format("`%1%:%2%:%3%'") % aterm2String(path) % line % column).str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ namespace nix {
|
||||||
|
|
||||||
|
|
||||||
MakeError(EvalError, Error)
|
MakeError(EvalError, Error)
|
||||||
|
MakeError(ParseError, Error)
|
||||||
MakeError(AssertionError, EvalError)
|
MakeError(AssertionError, EvalError)
|
||||||
MakeError(ThrownError, AssertionError)
|
MakeError(ThrownError, AssertionError)
|
||||||
MakeError(Abort, EvalError)
|
MakeError(Abort, EvalError)
|
||||||
|
|
|
@ -46,38 +46,88 @@ struct ParseData
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
static void duplicateAttr(ATerm name, ATerm pos, ATerm prevPos)
|
static string showAttrPath(ATermList attrPath)
|
||||||
{
|
{
|
||||||
throw EvalError(format("duplicate attribute `%1%' at %2% (previously defined at %3%)")
|
string s;
|
||||||
% aterm2String(name) % showPos(pos) % showPos (prevPos));
|
for (ATermIterator i(attrPath); i; ++i) {
|
||||||
|
if (!s.empty()) s += '.';
|
||||||
|
s += aterm2String(*i);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct Tree
|
||||||
|
{
|
||||||
|
Expr leaf; ATerm pos; bool recursive;
|
||||||
|
typedef std::map<ATerm, Tree> Children;
|
||||||
|
Children children;
|
||||||
|
Tree() { leaf = 0; recursive = true; }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
static ATermList buildAttrs(const Tree & t, ATermList & nonrec)
|
||||||
|
{
|
||||||
|
ATermList res = ATempty;
|
||||||
|
for (Tree::Children::const_reverse_iterator i = t.children.rbegin();
|
||||||
|
i != t.children.rend(); ++i)
|
||||||
|
if (!i->second.recursive)
|
||||||
|
nonrec = ATinsert(nonrec, makeBind(i->first, i->second.leaf, i->second.pos));
|
||||||
|
else
|
||||||
|
res = ATinsert(res, i->second.leaf
|
||||||
|
? makeBind(i->first, i->second.leaf, i->second.pos)
|
||||||
|
: makeBind(i->first, makeAttrs(buildAttrs(i->second, nonrec)), makeNoPos()));
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static Expr fixAttrs(bool recursive, ATermList as)
|
static Expr fixAttrs(bool recursive, ATermList as)
|
||||||
{
|
{
|
||||||
ATermList bs = ATempty, cs = ATempty;
|
Tree attrs;
|
||||||
ATermList * is = recursive ? &cs : &bs;
|
|
||||||
|
|
||||||
ATermMap used;
|
|
||||||
|
|
||||||
for (ATermIterator i(as); i; ++i) {
|
for (ATermIterator i(as); i; ++i) {
|
||||||
ATermList names; Expr src, e; ATerm name, pos;
|
ATermList names, attrPath; Expr src, e; ATerm name, pos;
|
||||||
|
|
||||||
if (matchInherit(*i, src, names, pos)) {
|
if (matchInherit(*i, src, names, pos)) {
|
||||||
bool fromScope = matchScope(src);
|
bool fromScope = matchScope(src);
|
||||||
for (ATermIterator j(names); j; ++j) {
|
for (ATermIterator j(names); j; ++j) {
|
||||||
Expr rhs = fromScope ? makeVar(*j) : makeSelect(src, *j);
|
Expr rhs = fromScope ? makeVar(*j) : makeSelect(src, *j);
|
||||||
if (used.get(*j)) duplicateAttr(*j, pos, used[*j]);
|
if (attrs.children.find(*j) != attrs.children.end())
|
||||||
used.set(*j, pos);
|
throw ParseError(format("duplicate definition of attribute `%1%' at %2%")
|
||||||
*is = ATinsert(*is, makeBind(*j, rhs, pos));
|
% showAttrPath(ATmakeList1(*j)) % showPos(pos));
|
||||||
|
Tree & t(attrs.children[*j]);
|
||||||
|
t.leaf = rhs; t.pos = pos; if (recursive) t.recursive = false;
|
||||||
}
|
}
|
||||||
} else if (matchBind(*i, name, e, pos)) {
|
|
||||||
if (used.get(name)) duplicateAttr(name, pos, used[name]);
|
|
||||||
used.set(name, pos);
|
|
||||||
bs = ATinsert(bs, *i);
|
|
||||||
} else abort(); /* can't happen */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return recursive? makeRec(bs, cs) : makeAttrs(bs);
|
else if (matchBindAttrPath(*i, attrPath, e, pos)) {
|
||||||
|
|
||||||
|
Tree * t(&attrs);
|
||||||
|
|
||||||
|
for (ATermIterator j(attrPath); j; ) {
|
||||||
|
name = *j; ++j;
|
||||||
|
if (t->leaf) throw ParseError(format("attribute set containing `%1%' at %2% already defined at %3%")
|
||||||
|
% showAttrPath(attrPath) % showPos(pos) % showPos (t->pos));
|
||||||
|
t = &(t->children[name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t->leaf)
|
||||||
|
throw ParseError(format("duplicate definition of attribute `%1%' at %2% and %3%")
|
||||||
|
% showAttrPath(attrPath) % showPos(pos) % showPos (t->pos));
|
||||||
|
if (!t->children.empty())
|
||||||
|
throw ParseError(format("duplicate definition of attribute `%1%' at %2%")
|
||||||
|
% showAttrPath(attrPath) % showPos(pos));
|
||||||
|
|
||||||
|
t->leaf = e; t->pos = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
else abort(); /* can't happen */
|
||||||
|
}
|
||||||
|
|
||||||
|
ATermList nonrec = ATempty;
|
||||||
|
ATermList rec = buildAttrs(attrs, nonrec);
|
||||||
|
|
||||||
|
return recursive ? makeRec(rec, nonrec) : makeAttrs(rec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -89,7 +139,7 @@ static void checkPatternVars(ATerm pos, ATermMap & map, Pattern pat)
|
||||||
ATermBool ellipsis;
|
ATermBool ellipsis;
|
||||||
if (matchVarPat(pat, name)) {
|
if (matchVarPat(pat, name)) {
|
||||||
if (map.get(name))
|
if (map.get(name))
|
||||||
throw EvalError(format("duplicate formal function argument `%1%' at %2%")
|
throw ParseError(format("duplicate formal function argument `%1%' at %2%")
|
||||||
% aterm2String(name) % showPos(pos));
|
% aterm2String(name) % showPos(pos));
|
||||||
map.set(name, name);
|
map.set(name, name);
|
||||||
}
|
}
|
||||||
|
@ -98,7 +148,7 @@ static void checkPatternVars(ATerm pos, ATermMap & map, Pattern pat)
|
||||||
ATerm d1;
|
ATerm d1;
|
||||||
if (!matchFormal(*i, name, d1)) abort();
|
if (!matchFormal(*i, name, d1)) abort();
|
||||||
if (map.get(name))
|
if (map.get(name))
|
||||||
throw EvalError(format("duplicate formal function argument `%1%' at %2%")
|
throw ParseError(format("duplicate formal function argument `%1%' at %2%")
|
||||||
% aterm2String(name) % showPos(pos));
|
% aterm2String(name) % showPos(pos));
|
||||||
map.set(name, name);
|
map.set(name, name);
|
||||||
}
|
}
|
||||||
|
@ -267,7 +317,7 @@ static void freeAndUnprotect(void * p)
|
||||||
%type <t> start expr expr_function expr_if expr_op
|
%type <t> start expr expr_function expr_if expr_op
|
||||||
%type <t> expr_app expr_select expr_simple bind inheritsrc formal
|
%type <t> expr_app expr_select expr_simple bind inheritsrc formal
|
||||||
%type <t> pattern pattern2
|
%type <t> pattern pattern2
|
||||||
%type <ts> binds ids expr_list string_parts ind_string_parts
|
%type <ts> binds ids attrpath expr_list string_parts ind_string_parts
|
||||||
%type <formals> formals
|
%type <formals> formals
|
||||||
%token <t> ID INT STR IND_STR PATH URI
|
%token <t> ID INT STR IND_STR PATH URI
|
||||||
%token IF THEN ELSE ASSERT WITH LET IN REC INHERIT EQ NEQ AND OR IMPL
|
%token IF THEN ELSE ASSERT WITH LET IN REC INHERIT EQ NEQ AND OR IMPL
|
||||||
|
@ -300,7 +350,7 @@ expr_function
|
||||||
| WITH expr ';' expr_function
|
| WITH expr ';' expr_function
|
||||||
{ $$ = makeWith($2, $4, CUR_POS); }
|
{ $$ = makeWith($2, $4, CUR_POS); }
|
||||||
| LET binds IN expr_function
|
| LET binds IN expr_function
|
||||||
{ $$ = makeSelect(fixAttrs(true, ATinsert($2, makeBind(toATerm("<let-body>"), $4, CUR_POS))), toATerm("<let-body>")); }
|
{ $$ = makeSelect(fixAttrs(true, ATinsert($2, makeBindAttrPath(ATmakeList1(toATerm("<let-body>")), $4, CUR_POS))), toATerm("<let-body>")); }
|
||||||
| expr_if
|
| expr_if
|
||||||
;
|
;
|
||||||
|
|
||||||
|
@ -391,8 +441,8 @@ binds
|
||||||
;
|
;
|
||||||
|
|
||||||
bind
|
bind
|
||||||
: ID '=' expr ';'
|
: attrpath '=' expr ';'
|
||||||
{ $$ = makeBind($1, $3, CUR_POS); }
|
{ $$ = makeBindAttrPath(ATreverse($1), $3, CUR_POS); }
|
||||||
| INHERIT inheritsrc ids ';'
|
| INHERIT inheritsrc ids ';'
|
||||||
{ $$ = makeInherit($2, $3, CUR_POS); }
|
{ $$ = makeInherit($2, $3, CUR_POS); }
|
||||||
;
|
;
|
||||||
|
@ -404,6 +454,11 @@ inheritsrc
|
||||||
|
|
||||||
ids: ids ID { $$ = ATinsert($1, $2); } | { $$ = ATempty; };
|
ids: ids ID { $$ = ATinsert($1, $2); } | { $$ = ATempty; };
|
||||||
|
|
||||||
|
attrpath
|
||||||
|
: attrpath '.' ID { $$ = ATinsert($1, $3); }
|
||||||
|
| ID { $$ = ATmakeList1($1); }
|
||||||
|
;
|
||||||
|
|
||||||
expr_list
|
expr_list
|
||||||
: expr_list expr_select { $$ = ATinsert($1, $2); }
|
: expr_list expr_select { $$ = ATinsert($1, $2); }
|
||||||
| { $$ = ATempty; }
|
| { $$ = ATempty; }
|
||||||
|
@ -453,12 +508,12 @@ static Expr parse(EvalState & state,
|
||||||
int res = yyparse(scanner, &data);
|
int res = yyparse(scanner, &data);
|
||||||
yylex_destroy(scanner);
|
yylex_destroy(scanner);
|
||||||
|
|
||||||
if (res) throw EvalError(data.error);
|
if (res) throw ParseError(data.error);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
checkVarDefs(state.primOps, data.result);
|
checkVarDefs(state.primOps, data.result);
|
||||||
} catch (Error & e) {
|
} catch (Error & e) {
|
||||||
throw EvalError(format("%1%, in `%2%'") % e.msg() % path);
|
throw ParseError(format("%1%, in `%2%'") % e.msg() % path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.result;
|
return data.result;
|
||||||
|
|
1
tests/lang/eval-okay-attrs3.exp
Normal file
1
tests/lang/eval-okay-attrs3.exp
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Str("foo 22 80 itchyxac",[])
|
22
tests/lang/eval-okay-attrs3.nix
Normal file
22
tests/lang/eval-okay-attrs3.nix
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
let
|
||||||
|
|
||||||
|
config =
|
||||||
|
{
|
||||||
|
services.sshd.enable = true;
|
||||||
|
services.sshd.port = 22;
|
||||||
|
services.httpd.port = 80;
|
||||||
|
hostName = "itchy";
|
||||||
|
a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z = "x";
|
||||||
|
foo = {
|
||||||
|
a = "a";
|
||||||
|
b.c = "c";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
in
|
||||||
|
if config.services.sshd.enable
|
||||||
|
then "foo ${toString config.services.sshd.port} ${toString config.services.httpd.port} ${config.hostName}"
|
||||||
|
+ "${config.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z}"
|
||||||
|
+ "${config.foo.a}"
|
||||||
|
+ "${config.foo.b.c}"
|
||||||
|
else "bar"
|
4
tests/lang/parse-fail-dup-attrs-4.nix
Normal file
4
tests/lang/parse-fail-dup-attrs-4.nix
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
services.ssh.port = 22;
|
||||||
|
services.ssh.port = 23;
|
||||||
|
}
|
4
tests/lang/parse-fail-dup-attrs-5.nix
Normal file
4
tests/lang/parse-fail-dup-attrs-5.nix
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
services.ssh = { enable = true; };
|
||||||
|
services.ssh.port = 23;
|
||||||
|
}
|
4
tests/lang/parse-fail-dup-attrs-6.nix
Normal file
4
tests/lang/parse-fail-dup-attrs-6.nix
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
services.ssh.port = 23;
|
||||||
|
services.ssh = { enable = true; };
|
||||||
|
}
|
Loading…
Reference in a new issue