From: Kaz Kylheku <kaz@kylheku.com>
Date: Tue, 13 Mar 2019 07:01:32 -0700
Subject: [PATCH] Implement "grouped targets" in ordinary rules.

This patch adds the + (plus) syntax:

  + tgt1 tgt2 ... tgtn : pre1 pre2 ...
        recipe

When the + is present, then the targets are understood to be built
together by one invocation of the recipe: each one lists the others in
its "also_make" list, similarly to what multiple-target pattern rules
already do.

* doc/make.texi: Section on Multiple Targets updated.

* read.c (enum make_word_type): New enum member, w_plus.
(record_files): New argument, are_also_makes flag which indicates
that the targets are all linked together as also-makes.
If this is true, then we build an also_make list which contains
each target represented as a dep. When all targets are registered,
we then make a second pass, installing a copy of this also_make
list into each target, possibly catenating it with an existing
also_make list.
(record_waiting_files): Pass new argument to record_files.
(eval): Check for the + token and set a new local variable
also_make_targets if that is so. This is then passed to
record_files by the record_waiting_files macro.
(get_next_mword): If a + is followed by whitespace, or end of string,
rathe than =, then instead of falling through, recognize it as the new
w_plus token.
---
 doc/make.texi | 41 ++++++++++++++++++++++++++++-----
 read.c        | 64 +++++++++++++++++++++++++++++++++++++++++++++++----
 2 files changed, 95 insertions(+), 10 deletions(-)

diff --git a/doc/make.texi b/doc/make.texi
index 01bcec7..033c72d 100644
--- a/doc/make.texi
+++ b/doc/make.texi
@@ -3012,13 +3012,27 @@ both pieces to the suffix list.  In practice, suffixes normally begin with
 @cindex targets, multiple
 @cindex rule, with multiple targets
 
-A rule with multiple targets is equivalent to writing many rules, each with
-one target, and all identical aside from that.  The same recipe applies to
-all the targets, but its effect may vary because you can substitute the
-actual target name into the recipe using @samp{$@@}.  The rule contributes
-the same prerequisites to all the targets also.
+When a rule has multiple targets, the semantics depends on what kind
+of rule it is, and whether the "grouped targets" feature is invoked.
 
-This is useful in two cases.
+There are two possible semantics. A rule with multiple targets may be
+equivalent to writing many rules, each with one target, and all identical aside
+from that.  Under this semantics, the same recipe applies to all the targets,
+but its effect may vary because you can substitute the actual target name into
+the recipe using @samp{$@@}.  The rule contributes the same prerequisites to
+all the targets also. This is known as "replicated recipe semantics".
+The other semantics is "grouped target" semantics. Under this semantics,
+the files are all understood to be updated or created together by a single
+invocation of the recipe.
+
+Pattern rules implicitly follow "grouped target" semantics.
+
+Ordinary rules use "replicated recipe" semantics by default. If
+the symbol @samp{+} is placed before the first target, separated
+from it by whitespace, then the rule follows "grouped target"
+semantics.
+
+"Replicated recipe semantics" is useful in two cases.
 
 @itemize @bullet
 @item
@@ -3064,6 +3078,21 @@ types of output, one if given @samp{-big} and one if given
 for an explanation of the @code{subst} function.
 @end itemize
 
+"Grouped target" semantics is useful when a recipe builds multiple
+files, which are further involved in dependencies. For instance,
+it can express a rule rule for building a parser using Yacc, whose
+recipe generates @samp{y.tab.c} and @samp{y.tab.h} at the same time.
+Both are made to be dependent on @samp{parser.y}. If neither one
+exists, or if both are out of date with respect to @samp{parser.y},
+the recipe is invoked only once. Furthermore, if for some reason
+just one of these two files is missing or out of date, GNU Make
+knows that running the recipe also updates the other.
+
+Note that under "grouped target" semantics, the @samp{$@@} variable
+still expands to a single target: it refers to that target which
+triggered the execution of the recipe. The recipe cannot rely on
+@samp{$@@} being a stable reference to a particular one of the targets.
+
 Suppose you would like to vary the prerequisites according to the
 target, much as the variable @samp{$@@} allows you to vary the recipe.
 You cannot do this with multiple targets in an ordinary rule, but you
diff --git a/read.c b/read.c
index b870aa8..83f48b7 100644
--- a/read.c
+++ b/read.c
@@ -71,7 +71,7 @@ struct vmodifiers
 enum make_word_type
   {
      w_bogus, w_eol, w_static, w_variable, w_colon, w_dcolon, w_semicolon,
-     w_varassign
+     w_varassign, w_plus
   };
 
 
@@ -141,7 +141,8 @@ static void do_undefine (char *name, enum variable_origin origin,
 static struct variable *do_define (char *name, enum variable_origin origin,
                                    struct ebuffer *ebuf);
 static int conditional_line (char *line, int len, const floc *flocp);
-static void record_files (struct nameseq *filenames, const char *pattern,
+static void record_files (struct nameseq *filenames, int are_also_makes,
+                          const char *pattern,
                           const char *pattern_percent, char *depstr,
                           unsigned int cmds_started, char *commands,
                           unsigned int commands_idx, int two_colon,
@@ -576,6 +577,7 @@ eval (struct ebuffer *ebuf, int set_default)
   unsigned int cmds_started, tgts_started;
   int ignoring = 0, in_ignored_define = 0;
   int no_targets = 0;           /* Set when reading a rule without targets.  */
+  int also_make_targets = 0;    /* Set when reading grouped targets. */
   struct nameseq *filenames = 0;
   char *depstr = 0;
   long nlines = 0;
@@ -593,7 +595,8 @@ eval (struct ebuffer *ebuf, int set_default)
         {                                                                     \
           fi.lineno = tgts_started;                                           \
           fi.offset = 0;                                                      \
-          record_files (filenames, pattern, pattern_percent, depstr,          \
+          record_files (filenames, also_make_targets, pattern,                \
+                        pattern_percent, depstr,                              \
                         cmds_started, commands, commands_idx, two_colon,      \
                         prefix, &fi);                                         \
           filenames = 0;                                                      \
@@ -601,6 +604,7 @@ eval (struct ebuffer *ebuf, int set_default)
       commands_idx = 0;                                                       \
       no_targets = 0;                                                         \
       pattern = 0;                                                            \
+      also_make_targets = 0;                                                  \
     } while (0)
 
   pattern_percent = 0;
@@ -1027,6 +1031,20 @@ eval (struct ebuffer *ebuf, int set_default)
            beginning, expanding as we go, and looking for "interesting"
            chars.  The first word is always expandable.  */
         wtype = get_next_mword (line, NULL, &lb_next, &wlen);
+
+        /* If the line starts with + followed by whitespace, then
+           it specifies that the targets are understood to be updated/created
+           together by a single invocation of the recipe. In this case,
+           we record a single entry for the first target which follows the +,
+           and install the remaining targets as the also_make list of deps
+           for that file, rather than iterating over the targets and replicating
+           the rule for each one. */
+        if (wtype == w_plus) {
+          also_make_targets = 1;
+          lb_next += wlen;
+          wtype = get_next_mword (lb_next, NULL, &lb_next, &wlen);
+        }
+
         switch (wtype)
           {
           case w_eol:
@@ -1931,7 +1949,8 @@ record_target_var (struct nameseq *filenames, char *defn,
    that are not incorporated into other data structures.  */
 
 static void
-record_files (struct nameseq *filenames, const char *pattern,
+record_files (struct nameseq *filenames, int are_also_makes,
+              const char *pattern,
               const char *pattern_percent, char *depstr,
               unsigned int cmds_started, char *commands,
               unsigned int commands_idx, int two_colon,
@@ -1939,6 +1958,7 @@ record_files (struct nameseq *filenames, const char *pattern,
 {
   struct commands *cmds;
   struct dep *deps;
+  struct dep *also_make = 0;
   const char *implicit_percent;
   const char *name;
 
@@ -2158,6 +2178,15 @@ record_files (struct nameseq *filenames, const char *pattern,
           f->cmds = cmds;
         }
 
+      if (are_also_makes)
+        {
+          struct dep *also = alloc_dep();
+          also->name = f->name;
+          also->file = f;
+          also->next = also_make;
+          also_make = also;
+        }
+
       f->is_target = 1;
 
       /* If this is a static pattern rule, set the stem to the part of its
@@ -2222,6 +2251,27 @@ record_files (struct nameseq *filenames, const char *pattern,
         O (error, flocp,
            _("*** mixed implicit and normal rules: deprecated syntax"));
     }
+
+  /* If the files are also-makes, then populate a copy of the also-make list
+     into each one. For the last file, we take our original also_make list
+     instead wastefully copying it one more time and freeing it. */
+  if (are_also_makes && also_make)
+    {
+      struct dep *i;
+
+      for (i = also_make; i != 0; i = i->next)
+        {
+          struct file *f = i->file;
+          struct dep *also_copy = i->next ? copy_dep_chain(also_make) : also_make;
+          struct dep *j;
+
+          for (j = also_copy; j->next != 0; j = j->next)
+            ;
+
+          j->next = f->also_make;
+          f->also_make = also_copy;
+        }
+    }
 }
 
 /* Search STRING for an unquoted STOPCHAR or blank (if BLANK is nonzero).
@@ -2676,6 +2726,12 @@ get_next_mword (char *buffer, char *delim, char **startp, unsigned int *length)
       break;
 
     case '+':
+      if (isspace(*p) || *p == 0)
+        {
+          wtype = w_plus;
+          break;
+        }
+      /* fallthrough */
     case '?':
     case '!':
       if (*p == '=')

