From d407d572fdc72f4eb14cc0f37d7d61446425b663 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 15 May 2009 12:35:23 +0000 Subject: [PATCH] * 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'. --- src/libexpr/nixexpr-ast.def | 1 + src/libexpr/nixexpr.cc | 2 +- src/libexpr/nixexpr.hh | 1 + src/libexpr/parser.y | 105 ++++++++++++++++++++------ tests/lang/eval-okay-attrs3.exp | 1 + tests/lang/eval-okay-attrs3.nix | 22 ++++++ tests/lang/parse-fail-dup-attrs-4.nix | 4 + tests/lang/parse-fail-dup-attrs-5.nix | 4 + tests/lang/parse-fail-dup-attrs-6.nix | 4 + 9 files changed, 118 insertions(+), 26 deletions(-) create mode 100644 tests/lang/eval-okay-attrs3.exp create mode 100644 tests/lang/eval-okay-attrs3.nix create mode 100644 tests/lang/parse-fail-dup-attrs-4.nix create mode 100644 tests/lang/parse-fail-dup-attrs-5.nix create mode 100644 tests/lang/parse-fail-dup-attrs-6.nix diff --git a/src/libexpr/nixexpr-ast.def b/src/libexpr/nixexpr-ast.def index ba8162984..00b5f5a13 100644 --- a/src/libexpr/nixexpr-ast.def +++ b/src/libexpr/nixexpr-ast.def @@ -70,6 +70,7 @@ Bool | ATermBool | Expr | Null | | Expr | Bind | string Expr Pos | ATerm | +BindAttrPath | ATermList Expr Pos | ATerm | # desugared during parsing Bind | string Expr | ATerm | ObsoleteBind Inherit | Expr ATermList Pos | ATerm | diff --git a/src/libexpr/nixexpr.cc b/src/libexpr/nixexpr.cc index d96b5be97..3675dccea 100644 --- a/src/libexpr/nixexpr.cc +++ b/src/libexpr/nixexpr.cc @@ -19,7 +19,7 @@ string showPos(ATerm pos) if (matchNoPos(pos)) return "undefined position"; if (!matchPos(pos, path, line, column)) throw badTerm("position expected", pos); - return (format("`%1%', line %2%") % aterm2String(path) % line).str(); + return (format("`%1%:%2%:%3%'") % aterm2String(path) % line % column).str(); } diff --git a/src/libexpr/nixexpr.hh b/src/libexpr/nixexpr.hh index 43ac19f92..c7cdf46df 100644 --- a/src/libexpr/nixexpr.hh +++ b/src/libexpr/nixexpr.hh @@ -11,6 +11,7 @@ namespace nix { MakeError(EvalError, Error) +MakeError(ParseError, Error) MakeError(AssertionError, EvalError) MakeError(ThrownError, AssertionError) MakeError(Abort, EvalError) diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index 9cdda3ee9..2ee3833fe 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -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%)") - % aterm2String(name) % showPos(pos) % showPos (prevPos)); + string s; + 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 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) { - ATermList bs = ATempty, cs = ATempty; - ATermList * is = recursive ? &cs : &bs; + Tree attrs; - ATermMap used; - 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)) { bool fromScope = matchScope(src); for (ATermIterator j(names); j; ++j) { Expr rhs = fromScope ? makeVar(*j) : makeSelect(src, *j); - if (used.get(*j)) duplicateAttr(*j, pos, used[*j]); - used.set(*j, pos); - *is = ATinsert(*is, makeBind(*j, rhs, pos)); + if (attrs.children.find(*j) != attrs.children.end()) + throw ParseError(format("duplicate definition of attribute `%1%' at %2%") + % 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 */ + } + + 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 */ } - return recursive? makeRec(bs, cs) : makeAttrs(bs); + 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; if (matchVarPat(pat, 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)); map.set(name, name); } @@ -98,7 +148,7 @@ static void checkPatternVars(ATerm pos, ATermMap & map, Pattern pat) ATerm d1; if (!matchFormal(*i, name, d1)) abort(); 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)); map.set(name, name); } @@ -267,7 +317,7 @@ static void freeAndUnprotect(void * p) %type start expr expr_function expr_if expr_op %type expr_app expr_select expr_simple bind inheritsrc formal %type pattern pattern2 -%type binds ids expr_list string_parts ind_string_parts +%type binds ids attrpath expr_list string_parts ind_string_parts %type formals %token ID INT STR IND_STR PATH URI %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 { $$ = makeWith($2, $4, CUR_POS); } | LET binds IN expr_function - { $$ = makeSelect(fixAttrs(true, ATinsert($2, makeBind(toATerm(""), $4, CUR_POS))), toATerm("")); } + { $$ = makeSelect(fixAttrs(true, ATinsert($2, makeBindAttrPath(ATmakeList1(toATerm("")), $4, CUR_POS))), toATerm("")); } | expr_if ; @@ -391,8 +441,8 @@ binds ; bind - : ID '=' expr ';' - { $$ = makeBind($1, $3, CUR_POS); } + : attrpath '=' expr ';' + { $$ = makeBindAttrPath(ATreverse($1), $3, CUR_POS); } | INHERIT inheritsrc ids ';' { $$ = makeInherit($2, $3, CUR_POS); } ; @@ -404,6 +454,11 @@ inheritsrc ids: ids ID { $$ = ATinsert($1, $2); } | { $$ = ATempty; }; +attrpath + : attrpath '.' ID { $$ = ATinsert($1, $3); } + | ID { $$ = ATmakeList1($1); } + ; + expr_list : expr_list expr_select { $$ = ATinsert($1, $2); } | { $$ = ATempty; } @@ -453,12 +508,12 @@ static Expr parse(EvalState & state, int res = yyparse(scanner, &data); yylex_destroy(scanner); - if (res) throw EvalError(data.error); + if (res) throw ParseError(data.error); try { checkVarDefs(state.primOps, data.result); } catch (Error & e) { - throw EvalError(format("%1%, in `%2%'") % e.msg() % path); + throw ParseError(format("%1%, in `%2%'") % e.msg() % path); } return data.result; diff --git a/tests/lang/eval-okay-attrs3.exp b/tests/lang/eval-okay-attrs3.exp new file mode 100644 index 000000000..d2c7555c1 --- /dev/null +++ b/tests/lang/eval-okay-attrs3.exp @@ -0,0 +1 @@ +Str("foo 22 80 itchyxac",[]) diff --git a/tests/lang/eval-okay-attrs3.nix b/tests/lang/eval-okay-attrs3.nix new file mode 100644 index 000000000..f29de11fe --- /dev/null +++ b/tests/lang/eval-okay-attrs3.nix @@ -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" diff --git a/tests/lang/parse-fail-dup-attrs-4.nix b/tests/lang/parse-fail-dup-attrs-4.nix new file mode 100644 index 000000000..77417432b --- /dev/null +++ b/tests/lang/parse-fail-dup-attrs-4.nix @@ -0,0 +1,4 @@ +{ + services.ssh.port = 22; + services.ssh.port = 23; +} diff --git a/tests/lang/parse-fail-dup-attrs-5.nix b/tests/lang/parse-fail-dup-attrs-5.nix new file mode 100644 index 000000000..f4b9efd0c --- /dev/null +++ b/tests/lang/parse-fail-dup-attrs-5.nix @@ -0,0 +1,4 @@ +{ + services.ssh = { enable = true; }; + services.ssh.port = 23; +} diff --git a/tests/lang/parse-fail-dup-attrs-6.nix b/tests/lang/parse-fail-dup-attrs-6.nix new file mode 100644 index 000000000..ae6d7a769 --- /dev/null +++ b/tests/lang/parse-fail-dup-attrs-6.nix @@ -0,0 +1,4 @@ +{ + services.ssh.port = 23; + services.ssh = { enable = true; }; +}