Nil as Lvalue

The result of an expression in Lua has to be assigned to a variable. The select function can pick one value from an argument list, but what about selecting all but one value?

This patch adds the following features:

  1. nil as an lvalue in regular assignment statements. This does what you’d expect, it skips the corresponding rvalue without creating an unnecessary local variable as assigning to _ would.
  2. nil as an lvalue in local statements. A local statement without nil acts exactly the same as before. When there is a nil, it acts like a local declaration followed by an assignment statement. i.e.
    local a,nil,b = 1,2,3

    is the same as

    local a,b
        a,nil,b = 1,2,3
  3. nil as the lvalue in a for statement. A dummy variable(s) with the name (nil) is created so there’s no upside to doing it, except for consistency with the other uses.

Formally, the syntax is

stat ::= varlist '=' explist |
         local varnamelist ['=' explist] |
         for varnamelist in explist do block end
 
varlist ::= var {',' var}
 
var ::= Name | prefixexp '[' exp ']' | prefixexp '.' Name | nil
 
varnamelist ::= varname {',' varname}
 
varname ::= Name | nil

I originally proposed this on the Lua mailing list.

Patch file

nil_lvalue.diff
diff --git a/src/lcode.c b/src/lcode.c
index 5e34624..e02befb 100644
--- a/src/lcode.c
+++ b/src/lcode.c
@@ -606,6 +606,10 @@ void luaK_storevar (FuncState *fs, expdesc *var, expdesc *ex) {
       luaK_codeABC(fs, op, var->u.ind.t, var->u.ind.idx, e);
       break;
     }
+    case VNIL: {
+      freeexp(fs, ex);
+      return;
+    }
     default: {
       lua_assert(0);  /* invalid var kind to store */
       break;
diff --git a/src/lparser.c b/src/lparser.c
index 9a54dfc..ed9c6ab 100644
--- a/src/lparser.c
+++ b/src/lparser.c
@@ -862,7 +862,7 @@ static void funcargs (LexState *ls, expdesc *f, int line) {
 */
 
 
-static void primaryexp (LexState *ls, expdesc *v) {
+static int primaryexp (LexState *ls, expdesc *v) {
   /* primaryexp -> NAME | '(' expr ')' */
   switch (ls->t.token) {
     case '(': {
@@ -871,14 +871,20 @@ static void primaryexp (LexState *ls, expdesc *v) {
       expr(ls, v);
       check_match(ls, ')', '(', line);
       luaK_dischargevars(ls->fs, v);
-      return;
+      return 1;
+    }
+    case TK_NIL: {
+      init_exp(v, VNIL, 0);
+      luaX_next(ls);
+      return 0;
     }
     case TK_NAME: {
       singlevar(ls, v);
-      return;
+      return 1;
     }
     default: {
       luaX_syntaxerror(ls, "unexpected symbol");
+      return 0;
     }
   }
 }
@@ -886,10 +892,11 @@ static void primaryexp (LexState *ls, expdesc *v) {
 
 static void suffixedexp (LexState *ls, expdesc *v) {
   /* suffixedexp ->
-       primaryexp { '.' NAME | '[' exp ']' | ':' NAME funcargs | funcargs } */
+       primaryexp { '.' NAME | '[' exp ']' | ':' NAME funcargs | funcargs } |
+       nil */
   FuncState *fs = ls->fs;
   int line = ls->linenumber;
-  primaryexp(ls, v);
+  if (!primaryexp(ls, v)) return;
   for (;;) {
     switch (ls->t.token) {
       case '.': {  /* fieldsel */
@@ -1140,7 +1147,7 @@ static void check_conflict (LexState *ls, struct LHS_assign *lh, expdesc *v) {
 
 static void assignment (LexState *ls, struct LHS_assign *lh, int nvars) {
   expdesc e;
-  check_condition(ls, vkisvar(lh->v.k), "syntax error");
+  check_condition(ls, (vkisvar(lh->v.k) | (lh->v.k==VNIL)), "syntax error");
   if (testnext(ls, ',')) {  /* assignment -> ',' suffixedexp assignment */
     struct LHS_assign nv;
     nv.prev = lh;
@@ -1312,13 +1319,16 @@ static void forbody (LexState *ls, int base, int line, int nvars, int isnum) {
 
 
 static void fornum (LexState *ls, TString *varname, int line) {
-  /* fornum -> NAME = exp1,exp1[,exp1] forbody */
+  /* fornum -> (NAME | nil) = exp1,exp1[,exp1] forbody */
   FuncState *fs = ls->fs;
   int base = fs->freereg;
   new_localvarliteral(ls, "(for index)");
   new_localvarliteral(ls, "(for limit)");
   new_localvarliteral(ls, "(for step)");
-  new_localvar(ls, varname);
+  if (varname)
+    new_localvar(ls, varname);
+  else
+    new_localvarliteral(ls, "(for variable)");
   checknext(ls, '=');
   exp1(ls);  /* initial value */
   checknext(ls, ',');
@@ -1334,7 +1344,7 @@ static void fornum (LexState *ls, TString *varname, int line) {
 
 
 static void forlist (LexState *ls, TString *indexname) {
-  /* forlist -> NAME {,NAME} IN explist forbody */
+  /* forlist -> (NAME | nil) {, (NAME | nil)} IN explist forbody */
   FuncState *fs = ls->fs;
   expdesc e;
   int nvars = 4;  /* gen, state, control, plus at least one declared var */
@@ -1345,10 +1355,19 @@ static void forlist (LexState *ls, TString *indexname) {
   new_localvarliteral(ls, "(for state)");
   new_localvarliteral(ls, "(for control)");
   /* create declared variables */
-  new_localvar(ls, indexname);
+  if (indexname)
+    new_localvar(ls, indexname);
+  else
+    new_localvarliteral(ls, "(for variable)");
   while (testnext(ls, ',')) {
-    new_localvar(ls, str_checkname(ls));
-    nvars++;
+    if (testnext(ls, TK_NIL)) {
+      new_localvarliteral(ls, "(nil)");
+      nvars++;
+    }
+    else {
+      new_localvar(ls, str_checkname(ls));
+      nvars++;
+    }
   }
   checknext(ls, TK_IN);
   line = ls->linenumber;
@@ -1365,7 +1384,8 @@ static void forstat (LexState *ls, int line) {
   BlockCnt bl;
   enterblock(fs, &bl, 1);  /* scope for loop and control variables */
   luaX_next(ls);  /* skip 'for' */
-  varname = str_checkname(ls);  /* first variable name */
+  varname = testnext(ls, TK_NIL) ? NULL
+            : str_checkname(ls);  /* first variable name */
   switch (ls->t.token) {
     case '=': fornum(ls, varname, line); break;
     case ',': case TK_IN: forlist(ls, varname); break;
@@ -1436,23 +1456,65 @@ static void localfunc (LexState *ls) {
 }
 
 
-static void localstat (LexState *ls) {
-  /* stat -> LOCAL NAME {',' NAME} ['=' explist] */
+static int localstat (LexState *ls, int prev, int level) {
+  /* stat -> LOCAL (NAME | nil) {',' (NAME | nil)} ['=' explist] */
+  int first = ls->fs->nactvar + prev;
   int nvars = 0;
+  int total;
   int nexps;
   expdesc e;
   do {
-    new_localvar(ls, str_checkname(ls));
-    nvars++;
+    if (testnext(ls, TK_NIL)) {
+      /* slot is empty, push and handle the next group of assignments */
+      if (testnext(ls, ',')) {
+        nexps = localstat(ls, prev+nvars, level+1);
+        if (nexps == 0) return 0;
+        if (nexps > prev+nvars) {  /* drop the unused value */
+          ls->fs->freereg--;
+          nexps--;
+        }
+        goto lexp;
+      } else
+        break;
+    }
+    else {
+      new_localvar(ls, str_checkname(ls));
+      nvars++;
+    }
   } while (testnext(ls, ','));
+  if (level > 0) {
+    /* assignment list has holes, initialize the vars */
+    e.k = VVOID;
+    adjust_assign(ls, prev+nvars, 0, &e);
+    adjustlocalvars(ls, prev+nvars);
+  }
   if (testnext(ls, '='))
     nexps = explist(ls, &e);
   else {
+    if (level > 0) return 0;
     e.k = VVOID;
     nexps = 0;
   }
-  adjust_assign(ls, nvars, nexps, &e);
-  adjustlocalvars(ls, nvars);
+  /* each level is one nil */
+  total = level + prev + nvars;
+  adjust_assign(ls, total, nexps, &e);
+  if (level == 0) {
+    /* no holes in the assignment list, set vars in-place */
+    adjustlocalvars(ls, total);
+    return 0;
+  }
+  if (nexps > total)
+    ls->fs->freereg -= nexps - total;  /* remove extra values */
+  nexps = total;
+  lexp:
+  while (nvars-- > 0) {
+    expdesc var;
+    init_exp(&var, VLOCAL, first+nvars);
+    init_exp(&e, VNONRELOC, ls->fs->freereg-1);
+    luaK_storevar(ls->fs, &var, &e);
+    nexps--;
+  }
+  return nexps;
 }
 
 
@@ -1570,7 +1632,7 @@ static void statement (LexState *ls) {
       if (testnext(ls, TK_FUNCTION))  /* local function? */
         localfunc(ls);
       else
-        localstat(ls);
+        localstat(ls, 0, 0);
       break;
     }
     case TK_DBCOLON: {  /* stat -> label */