From: Kaz Kylheku <kaz@kylheku.com>
Date: Thu, 14 Mar 2019 06:45:13 -0700
Subject: [PATCH] Implement "grouped targets" in ordinary rules.

This patch adds the &: (amperand colon) syntax:

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

When the & is present (either on a single or double colon), 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.

* main.c (initialize_stopchar_map): Assign new MAP_AMPERSAND
bitmask to '&' character's table entry.
* makeint.h (MAP_AMPERSAND): New category bitmask for string searches
terminated by characters falling into a category.
* read.c (enum make_word_type): New enum members, w_ampcolon and
w_ampdcolon.
(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): Adding parsing logic in several places for the new &: and
&:: separator. If the ampersand is present, the new local Boolean
variable also_make_targets is set. This is then passed to
record_files by the record_waiting_files macro.
(get_next_mword): Recognize &: and &:: and return w_ampcolon
and w_ampdcolon. A header coment is updated here to mention the
two new token types.

* tests/scripts/features/grouped_targets: New test script.

diff --git a/doc/make.texi b/doc/make.texi
index 01bcec7..caf531c 100644
--- a/doc/make.texi
+++ b/doc/make.texi
@@ -3012,13 +3012,51 @@ 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, it has two possible semantics:
+independent-target semantics or grouped-target semantics.
 
-This is useful in two cases.
+Regardless of semantics, 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 each 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.
+
+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
+can do it with a @dfn{static pattern rule}.  @xref{Static Pattern,
+,Static Pattern Rules}.
+
+A multiple target rule with independent-target semantics is understood
+to be expressing completely separate rules.  The rule's recipe is executed
+for each target which needs updating. If three of the targets require updating,
+then the recipe is executed three times, with the @samp{$@@} automatic variable
+taking on the name of each of these three targets in the corresponding
+executions of the recipe.
+
+A multiple target rule with grouped-target semantics considers all of the
+targets to belong into a group which is updated together by a single
+invocation of the recipe. If one or more of the targets requires updating,
+the rule is executed only once. After executing the recipe once, Make assumes
+that all of the targets in the group are up-to-date as far as that rule
+is concerned, or remain so if they already were, and that all these targets
+may have been updated by that rules, so that all rules which have any of those
+targets are prerequisites need to be considered for possible activation.
+
+When a grouped-target rule's recipe executes, the automatic variable
+@samp{$@@} is set to the name of that particular target which triggered the
+rule. Caution must be used if relying on this variable in the recipe of
+a grouped-target rule.
+
+Pattern rules with multiple target patterns implicitly grouped-target semantics
+for the files which match those patterns.
+
+Explicit rules with multiple targets use independent-target semantics by
+default. In an explicit rule, if the prerequisite separator @samp{:} character
+is immediately prefixed with the ampersand character @samp{&}, then
+that rule uses grouped-semantics.
+
+Multiple target patterns with independent-semantics are useful in two cases.
 
 @itemize @bullet
 @item
@@ -3064,11 +3102,40 @@ types of output, one if given @samp{-big} and one if given
 for an explanation of the @code{subst} function.
 @end itemize
 
-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
-can do it with a @dfn{static pattern rule}.  @xref{Static Pattern,
-,Static Pattern Rules}.
+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},
+which happens whenever @samp{parser.y} is edited,
+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
+is informed that running the recipe has updated the other.
+
+A situation is possible whereby a given target appears in two or more rules
+which use grouped-target semantics, in combination with different groups of other
+targets. For instance, suppose targets @samp{A}, @samp{B} and @samp{C} are
+grouped in one rule, and @samp{C}, @samp{D} and @samp{E} are grouped in another
+rule appearing later. In this situation, the behavior depends on whether
+the rules are ordinary or double-colon rules. (@pxref{Double-Colon}).
+
+If they are ordinary rules, then Make will issue a warning message and
+partially dissociate @samp{C} from the @samp{A}-@samp{B}-@samp{C} group.  If
+@samp{C} requires updating, it is no longer the case that @samp{A} and @samp{B}
+require updating; however, the reverse relationship still holds:
+if @samp{A} or @samp{B} require updating then, do does @samp{C}.
+This situation should be avoided.
+
+If all of the grouped-target rules with overlapping target groups are
+double-colon rules, then the behavior is that the groups are effectively
+independent. Each grouped double-colon rule's recipe is executed at most once,
+if at least one of its multiple targets requires updating.
+This applies to the common targets without regard for their duplication.
+In the foregoing example, if targets @samp{C} is considered out of
+date by both rules, then both rules will activate and execute their recipe.
+Grouped-target double colon rules are specified using the @samp{&::}
+separator: an ampersand prefixed to the double colon.
 
 @node Multiple Rules, Static Pattern, Multiple Targets, Rules
 @section Multiple Rules for One Target
diff --git a/main.c b/main.c
index fa8045f..0463caa 100644
--- a/main.c
+++ b/main.c
@@ -642,6 +642,7 @@ initialize_stopchar_map (void)
   stopchar_map[(int)';'] = MAP_SEMI;
   stopchar_map[(int)'='] = MAP_EQUALS;
   stopchar_map[(int)':'] = MAP_COLON;
+  stopchar_map[(int)'&'] = MAP_AMPERSAND;
   stopchar_map[(int)'%'] = MAP_PERCENT;
   stopchar_map[(int)'|'] = MAP_PIPE;
   stopchar_map[(int)'.'] = MAP_DOT | MAP_USERFUNC;
diff --git a/makeint.h b/makeint.h
index 8f718eb..ebe4029 100644
--- a/makeint.h
+++ b/makeint.h
@@ -393,6 +393,7 @@ extern int unixy_shell;
 #define MAP_PIPE        0x0100
 #define MAP_DOT         0x0200
 #define MAP_COMMA       0x0400
+#define MAP_AMPERSAND   0x0800
 
 /* These are the valid characters for a user-defined function.  */
 #define MAP_USERFUNC    0x2000
diff --git a/read.c b/read.c
index b870aa8..fb7f744 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_ampcolon, w_ampdcolon
   };
 
 
@@ -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,7 @@ 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);
+
         switch (wtype)
           {
           case w_eol:
@@ -1038,6 +1043,8 @@ eval (struct ebuffer *ebuf, int set_default)
 
           case w_colon:
           case w_dcolon:
+          case w_ampcolon:
+          case w_ampdcolon:
             /* We accept and ignore rules without targets for
                compatibility with SunOS 4 make.  */
             no_targets = 1;
@@ -1082,16 +1089,28 @@ eval (struct ebuffer *ebuf, int set_default)
                   }
               }
 
-            colonp = find_char_unquote (p2, MAP_COLON);
+            colonp = find_char_unquote (p2, MAP_COLON | MAP_AMPERSAND);
+
 #ifdef HAVE_DOS_PATHS
             /* The drive spec brain-damage strikes again...  */
             /* Note that the only separators of targets in this context
                are whitespace and a left paren.  If others are possible,
                they should be added to the string in the call to index.  */
-            while (colonp && (colonp[1] == '/' || colonp[1] == '\\') &&
-                   colonp > p2 && isalpha ((unsigned char)colonp[-1]) &&
-                   (colonp == p2 + 1 || strchr (" \t(", colonp[-2]) != 0))
-              colonp = find_char_unquote (colonp + 1, MAP_COLON);
+            while (colonp &&
+                   ((colonp[0] != '&' &&
+                     (colonp[1] == '/' || colonp[1] == '\\') &&
+                     colonp > p2 && isalpha ((unsigned char)colonp[-1]) &&
+                     (colonp == p2 + 1 || strchr (" \t(", colonp[-2]) != 0)) ||
+                    (colonp[0] == '&') && colonp[1] != ':'))
+              colonp = find_char_unquote (colonp + 1, MAP_COLON | MAP_AMPERSAND);
+#else
+            /* If we stopped on an ampersand, that only counts if
+               it is immediately followed by a colon. If it isn't,
+               we must keep searching. Note that in this case we don't care
+               about DOS drive letters, because &:/ or &:\ isn't a
+               drive spec. */
+            while (colonp && colonp[0] == '&' && colonp[1] != ':')
+              colonp = find_char_unquote (colonp + 1, MAP_COLON | MAP_AMPERSAND);
 #endif
             if (colonp != 0)
               break;
@@ -1126,12 +1145,21 @@ eval (struct ebuffer *ebuf, int set_default)
               O (fatal, fstart, _("missing separator"));
           }
 
-        /* Make the colon the end-of-string so we know where to stop
-           looking for targets.  Start there again once we're done.  */
-        *colonp = '\0';
-        filenames = PARSE_SIMPLE_SEQ (&p2, struct nameseq);
-        *colonp = ':';
-        p2 = colonp;
+        {
+          char save = *colonp;
+
+          /* If we have &:, it specifies that the targets are understood to be
+             updated/created together by a single invocation of the recipe. */
+          if (save == '&')
+            also_make_targets = 1;
+
+          /* Make the colon the end-of-string so we know where to stop
+             looking for targets.  Start there again once we're done.  */
+          *colonp = '\0';
+          filenames = PARSE_SIMPLE_SEQ (&p2, struct nameseq);
+          *colonp = save;
+          p2 = colonp + (save == '&');
+        }
 
         if (!filenames)
           {
@@ -1255,7 +1283,8 @@ eval (struct ebuffer *ebuf, int set_default)
         if (p != 0)
           {
             struct nameseq *target;
-            target = PARSE_FILE_SEQ (&p2, struct nameseq, MAP_COLON, NULL,
+            target = PARSE_FILE_SEQ (&p2, struct nameseq,
+                                     MAP_COLON, NULL,
                                      PARSEFS_NOGLOB);
             ++p2;
             if (target == 0)
@@ -1931,7 +1960,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 +1969,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 +2189,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 +2262,30 @@ 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;
+
+          if (f->also_make != 0)
+            {
+              error (&cmds->fileinfo, strlen (f->name),
+                     _("warning: overriding group membership for target '%s'"),
+                     f->name);
+              free_dep_chain (f->also_make);
+            }
+
+          f->also_make = also_copy;
+        }
+    }
 }
 
 /* Search STRING for an unquoted STOPCHAR or blank (if BLANK is nonzero).
@@ -2619,6 +2683,8 @@ readline (struct ebuffer *ebuf)
      w_variable     A word containing one or more variables/functions
      w_colon        A colon
      w_dcolon       A double-colon
+     w_ampcolon     An ampersand-colon (&:) token
+     w_ampdcolon    An ampersand-double-colon (&::) token
      w_semicolon    A semicolon
      w_varassign    A variable assignment operator (=, :=, ::=, +=, ?=, or !=)
 
@@ -2674,7 +2740,18 @@ get_next_mword (char *buffer, char *delim, char **startp, unsigned int *length)
           break;
         }
       break;
-
+    case '&':
+      if (*p == ':')
+        {
+          ++p;
+          wtype = w_ampcolon;
+          if (*p == ':')
+            {
+              ++p;
+              wtype = w_ampdcolon;
+            }
+        }
+      break;
     case '+':
     case '?':
     case '!':
@@ -2682,15 +2759,16 @@ get_next_mword (char *buffer, char *delim, char **startp, unsigned int *length)
         {
           ++p;
           wtype = w_varassign;
-          break;
         }
-
+      break;
     default:
-      if (delim && strchr (delim, c))
-        wtype = w_static;
       break;
     }
 
+  /* Fallback case: delim is supplied and c is one of those characters. */
+  if (wtype == w_bogus && delim && strchr (delim, c))
+    wtype = w_static;
+
   /* Did we find something?  If so, return now.  */
   if (wtype != w_bogus)
     goto done;
@@ -2777,6 +2855,10 @@ get_next_mword (char *buffer, char *delim, char **startp, unsigned int *length)
             }
           break;
 
+        case '&':
+          if (*p == ':')
+            goto done_word;
+          /* fallthrough */
         default:
           if (delim && strchr (delim, c))
             goto done_word;
diff --git a/tests/scripts/features/grouped_targets b/tests/scripts/features/grouped_targets
new file mode 100644
index 0000000..dc83118
--- /dev/null
+++ b/tests/scripts/features/grouped_targets
@@ -0,0 +1,104 @@
+#                                                                    -*-perl-*-
+
+$description = "This test is about grouped multiple targets indicated by &:";
+$details = "Here we test for requirements like\n"
+          ."- if multiple such targets are updated, the recipe is run once\n"
+          ."- parsing issues related to the &: syntax itself\n";
+
+# Parsing: &: allowed without any targets.
+run_make_test(q{
+.PHONY: all
+&:;
+all: ;@echo -n
+},
+'', "");
+
+# Parsing: &: works not preceded by whitespace.
+run_make_test(q{
+foo&:;@echo foo
+},
+'foo', "foo");
+
+# Ordinary rule runs recipe four times for t1 t2 t3 t4.
+# Grouped target rule runs recipe once; others are considered updated.
+run_make_test(q{
+.PHONY: t1 t2 t3 t4 g1 g2 g3 g4
+t1 t2 t3 t4: ; @echo $@
+g1 g2 g3 g4 &: ; @echo $@
+},
+'t1 t2 t3 t4 g1 g2 g3 g4',
+"t1\n"
+."t2\n"
+."t3\n"
+."t4\n"
+."g1\n"
+."make: Nothing to be done for 'g2'.\n"
+."make: Nothing to be done for 'g3'.\n"
+."make: Nothing to be done for 'g4'.");
+
+# Similar to previous test, but targets come from m1 phony
+# rather than from the command line. We don't see "Nothing to
+# be done for" messages. Also, note reversed order g4 g3 ...
+# Thus the auto variable $@ is "g4" when that rule fires.
+run_make_test(q{
+.PHONY: m1 t1 t2 t3 t4 g1 g2 g3 g4
+m1: t1 t2 t3 t4 g4 g3 g2 g1
+t1 t2 t3 t4: ; @echo $@
+g1 g2 g3 g4&: ; @echo $@
+},
+'',
+"t1\nt2\nt3\nt4\ng4");
+
+# Double-colon grouped target rules.
+run_make_test(q{
+.PHONY: M a b c d e f g h
+M: a b
+a b c&:: ; @echo X
+c d e&:: ; @echo Y
+f g h&:: ; @echo Z
+},
+'',
+"X");
+
+run_make_test(q{
+.PHONY: M a b c d e f g h
+M: c
+a b c&:: ; @echo X
+c d e&:: ; @echo Y
+f g h&:: ; @echo Z
+},
+'',
+"X\nY");
+
+run_make_test(q{
+.PHONY: M a b c d e f g h
+M: a b c d e
+a b c&:: ; @echo X
+c d e&:: ; @echo Y
+f g h&:: ; @echo Z
+},
+'',
+"X\nY");
+
+run_make_test(q{
+.PHONY: M a b c d e f g h
+M: d e
+a b c&:: ; @echo X
+c d e&:: ; @echo Y
+f g h&:: ; @echo Z
+},
+'',
+"Y");
+
+run_make_test(q{
+.PHONY: M a b c d e f g h
+M: f g h
+a b c&:: ; @echo X
+c d e&:: ; @echo Y
+f g h&:: ; @echo Z
+},
+'',
+"Z");
+
+# This tells the test driver that the perl test script executed properly.
+1;
