From 6c06c547dcc990a9db0648d66b6a6519c3d1e5d4 Mon Sep 17 00:00:00 2001
From: Paul Smith <psmith@gnu.org>
Date: Sat, 18 Sep 2021 18:53:38 -0400
Subject: [PATCH] Add support for the POSIX :::= assignment operator.

POSIX Issue 8 will require a new assignment operator, :::=.
This operator behaves similarly to the BSD make := operator: the
right-hand side is expanded immediately, but then the value is
re-escaped (all '$' are converted to '$$') and the resulting variable
is considered a recursive variable: the value is re-expanded on use.

* src/variable.h (enum variable_flavor): Add f_expand flavor.
* src/variable.c (do_variable_definition): When defining f_expand,
post-process the result to re-escape '$' characters.
Remove default: to the compiler warns about un-handled enum values.
Set recursive values for both f_recursive and f_expand.
(parse_variable_definition): Rewrite this method.
The previous version was annoying to extend to ':::='.
(print_variable): Remove default: so the compiler warns us about
un-handled enum values.
* src/function.c (func_origin): Remove default: so the compiler warns
us about un-handled enum values.
* doc/make.texi: Add documentation for :::=.
* tests/scripts/variables/define: Add a test for define :::=.
* tests/scripts/variables/flavors: Add tests for :::=.
* tests/scripts/variables/negative: Add tests for :::=.
---
 doc/make.texi                    | 227 ++++++++++++++++++++++---------
 src/function.c                   |   1 -
 src/makeint.h                    |   2 +-
 src/variable.c                   | 223 +++++++++++++++++-------------
 src/variable.h                   |   1 +
 tests/scripts/variables/define   |  11 +-
 tests/scripts/variables/flavors  |  44 ++++++
 tests/scripts/variables/negative |  14 ++
 8 files changed, 363 insertions(+), 160 deletions(-)

diff --git a/doc/make.texi b/doc/make.texi
index 4470386d..53648b0a 100644
--- a/doc/make.texi
+++ b/doc/make.texi
@@ -258,6 +258,13 @@ How to Use Variables
 * Suppressing Inheritance::     Suppress inheritance of variables.
 * Special Variables::           Variables with special meaning or behavior.
 
+The Two Flavors of Variables
+
+* Recursive Assignment::        Setting recursively expanded variables.
+* Simple Assignment::           Setting simply expanded variables.
+* Immediate Assignment::        Setting immediately expanded variables.
+* Conditional Assignment::      Assigning variable values conditionally.
+
 Advanced Features for Reference to Variables
 
 * Substitution Refs::           Referencing a variable with
@@ -1528,6 +1535,7 @@ Variable definitions are parsed as follows:
 @var{immediate} ?= @var{deferred}
 @var{immediate} := @var{immediate}
 @var{immediate} ::= @var{immediate}
+@var{immediate} :::= @var{immediate-with-escape}
 @var{immediate} += @var{deferred} or @var{immediate}
 @var{immediate} != @var{immediate}
 
@@ -1551,6 +1559,10 @@ define @var{immediate} ::=
   @var{immediate}
 endef
 
+define @var{immediate} :::=
+  @var{immediate-with-escape}
+endef
+
 define @var{immediate} +=
   @var{deferred} or @var{immediate}
 endef
@@ -1564,6 +1576,11 @@ For the append operator @samp{+=}, the right-hand side is considered
 immediate if the variable was previously set as a simple variable
 (@samp{:=} or @samp{::=}), and deferred otherwise.
 
+For the @var{immediate-with-escape} operator @samp{:::=}, the value on
+the right-hand side is immediately expanded but then escaped (that is,
+all instances of @code{$} in the result of the expansion are replaced
+with @code{$$}).
+
 For the shell assignment operator @samp{!=}, the right-hand side is
 evaluated immediately and handed to the shell.  The result is stored
 in the variable named on the left, and that variable is considered a
@@ -5357,10 +5374,20 @@ often improved is automatic variables (@pxref{Automatic Variables}).
 @cindex recursively expanded variables
 @cindex variables, recursively expanded
 
-There are two ways that a variable in GNU @code{make} can have a value;
-we call them the two @dfn{flavors} of variables.  The two flavors are
-distinguished in how they are defined and in what they do when expanded.
+There are different ways that a variable in GNU @code{make} can get a value;
+we call them the @dfn{flavors} of variables.  The flavors are distinguished in
+how they handle the values they are assigned in the makefile, and in how those
+values are managed when the variable is later used and expanded.
 
+@menu
+* Recursive Assignment::        Setting recursively expanded variables.
+* Simple Assignment::           Setting simply expanded variables.
+* Immediate Assignment::        Setting immediately expanded variables.
+* Conditional Assignment::      Assigning variable values conditionally.
+@end menu
+
+@node Recursive Assignment, Simple Assignment, Flavors, Flavors
+@subsection Recursively Expanded Variable Assignment
 @cindex =
 The first flavor of variable is a @dfn{recursively expanded} variable.
 Variables of this sort are defined by lines using @samp{=}
@@ -5417,7 +5444,9 @@ expanded.  This makes @code{make} run slower; worse, it causes the
 because you cannot easily control when they are called, or even how many
 times.
 
-To avoid all the problems and inconveniences of recursively expanded
+@node Simple Assignment, Immediate Assignment, Recursive Assignment, Flavors
+@subsection Simply Expanded Variable Assignment
+To avoid the problems and inconveniences of recursively expanded
 variables, there is another flavor: simply expanded variables.
 
 @cindex simply expanded variables
@@ -5427,34 +5456,35 @@ variables, there is another flavor: simply expanded variables.
 @dfn{Simply expanded variables} are defined by lines using @samp{:=}
 or @samp{::=} (@pxref{Setting, ,Setting Variables}).  Both forms are
 equivalent in GNU @code{make}; however only the @samp{::=} form is
-described by the POSIX standard (support for @samp{::=} was added to
-the POSIX standard in 2012, so older versions of @code{make} won't
-accept this form either).
+described by the POSIX standard (support for @samp{::=} is added to
+the POSIX standard for POSIX Issue 8).
 
-The value of a simply expanded variable is scanned
-once and for all, expanding any references to other variables and
-functions, when the variable is defined.  The actual value of the simply
-expanded variable is the result of expanding the text that you write.
-It does not contain any references to other variables; it contains their
-values @emph{as of the time this variable was defined}.  Therefore,
+The value of a simply expanded variable is scanned once, expanding any
+references to other variables and functions, when the variable is
+defined.  Once that expansion is complete the value of the variable is
+never expanded again: when the variable is used the value is copied
+verbatim as the expansion.  If the value contained variable references
+the result of the expansion will contain their values @emph{as of the
+time this variable was defined}.  Therefore,
 
 @example
+@group
 x := foo
 y := $(x) bar
 x := later
+@end group
 @end example
 
 @noindent
 is equivalent to
 
 @example
+@group
 y := foo bar
 x := later
+@end group
 @end example
 
-When a simply expanded variable is referenced, its value is substituted
-verbatim.
-
 Here is a somewhat more complicated example, illustrating the use of
 @samp{:=} in conjunction with the @code{shell} function.
 (@xref{Shell Function, , The @code{shell} Function}.)  This example
@@ -5503,8 +5533,10 @@ this means you can include leading spaces in a variable value by
 protecting them with variable references, like this:
 
 @example
+@group
 nullstring :=
 space := $(nullstring) # end of the line
+@end group
 @end example
 
 @noindent
@@ -5528,6 +5560,84 @@ Here the value of the variable @code{dir} is @w{@samp{/foo/bar    }}
 (with four trailing spaces), which was probably not the intention.
 (Imagine something like @w{@samp{$(dir)/file}} with this definition!)
 
+@node Immediate Assignment, Conditional Assignment, Simple Assignment, Flavors
+@subsection Immediately Expanded Variable Assignment
+@cindex immediate variable assignment
+@cindex variables, immediate assignment
+@cindex :::=
+
+Another form of assignment allows for immediate expansion, but unlike simple
+assignment the resulting variable is recursive: it will be re-expanded again
+on every use.  In order to avoid unexpected results, after the value is
+immediately expanded it will automatically be quoted: all instances of
+@code{$} in the value after expansion will be converted into @code{$$}.  This
+type of assignment uses the @samp{:::=} operator.  For example,
+
+@example
+@group
+var = first
+OUT :::= $(var)
+var = second
+@end group
+@end example
+
+@noindent
+results in the @code{OUT} variable containing the text @samp{first}, while here:
+
+@example
+@group
+var = one$$two
+OUT :::= $(var)
+var = three$$four
+@end group
+@end example
+
+@noindent
+results in the @code{OUT} variable containing the text @samp{one$$two}.  The
+value is expanded when the variable is assigned, so the result is the
+expansion of the first value of @code{var}, @samp{one$two}; then the value is
+re-escaped before the assignment is complete giving the final result of
+@samp{one$$two}.
+
+The variable @code{OUT} is thereafter considered a recursive variable, so it
+will be re-expanded when it is used.
+
+This seems functionally equivalent to the @samp{:=} / @samp{::=} operators,
+but there are a few differences:
+
+First, after assignment the variable is a normal recursive variable; when you
+append to it with @samp{+=} the value on the right-hand side is not expanded
+immediately.  If you prefer the @samp{+=} operator to expand the right-hand
+side immediately you should use the @samp{:=} / @samp{::=} assignment instead.
+
+Second, these variables are slightly less efficient than simply expanded
+variables since they do need to be re-expanded when they are used, rather than
+merely copied.  However since all variable references are escaped this
+expansion simply un-escapes the value, it won't expand any variables or run
+any functions.
+
+Here is another example:
+
+@example
+@group
+var = one$$two
+OUT :::= $(var)
+OUT += $(var)
+var = three$$four
+@end group
+@end example
+
+After this, the value of @code{OUT} is the text @samp{one$$two $(var)}.  When
+this variable is used it will be expanded and the result will be
+@samp{one$two three$four}.
+
+This style of assignment is equivalent to the traditional BSD @code{make}
+@samp{:=} operator; as you can see it works slightly differently than the GNU
+@code{make} @samp{:=} operator.  The @code{:::=} operator is added to the
+POSIX specification in Issue 8 to provide portability.
+
+@node Conditional Assignment, , Immediate Assignment, Flavors
+@subsection Conditional Variable Assignment
 @cindex conditional variable assignment
 @cindex variables, conditional assignment
 @cindex ?=
@@ -5632,12 +5742,9 @@ sets @samp{bar} to @samp{a.c b.c l.a c.c}.
 @cindex @code{$}, in variable name
 @cindex dollar sign (@code{$}), in variable name
 
-Computed variable names are a complicated concept needed only for
-sophisticated makefile programming.  For most purposes you need not
-consider them, except to know that making a variable with a dollar sign
-in its name might have strange results.  However, if you are the type
-that wants to understand everything, or you are actually interested in
-what they do, read on.
+Computed variable names are an advanced concept, very useful in more
+sophisticated makefile programming.  In simple situations you need not
+consider them, but they can be extremely useful.
 
 Variables may be referenced inside the name of a variable.  This is
 called a @dfn{computed variable name} or a @dfn{nested variable
@@ -5685,10 +5792,9 @@ a := $($(x))
 defines @code{a} as @samp{Hello}: @samp{$($(x))} becomes @samp{$($(y))}
 which becomes @samp{$(z)} which becomes @samp{Hello}.
 
-Nested variable references can also contain modified references and
-function invocations (@pxref{Functions, ,Functions for Transforming Text}),
-just like any other reference.
-For example, using the @code{subst} function
+Nested variable references can also contain modified references and function
+invocations (@pxref{Functions, ,Functions for Transforming Text}), just like
+any other reference.  For example, using the @code{subst} function
 (@pxref{Text Functions, ,Functions for String Substitution and Analysis}):
 
 @example
@@ -5857,27 +5963,30 @@ Several variables have constant initial values.
 @cindex =
 @cindex :=
 @cindex ::=
+@cindex :::=
 @cindex ?=
 @cindex !=
 
-To set a variable from the makefile, write a line starting with the
-variable name followed by @samp{=}, @samp{:=}, or @samp{::=}.  Whatever
-follows the @samp{=}, @samp{:=}, or @samp{::=} on the line becomes the
-value.  For example,
+To set a variable from the makefile, write a line starting with the variable
+name followed by one of the assignment operators @samp{=}, @samp{:=},
+@samp{::=}, or @samp{:::=}.  Whatever follows the operator and any initial
+whitespace on the line becomes the value.  For example,
 
 @example
 objects = main.o foo.o bar.o utils.o
 @end example
 
 @noindent
-defines a variable named @code{objects}.  Whitespace around the variable
-name and immediately after the @samp{=} is ignored.
+defines a variable named @code{objects} to contain the value @samp{main.o
+foo.o bar.o utils.o}.  Whitespace around the variable name and immediately
+after the @samp{=} is ignored.
 
-Variables defined with @samp{=} are @dfn{recursively expanded}
-variables.  Variables defined with @samp{:=} or @samp{::=} are
-@dfn{simply expanded} variables; these definitions can contain
-variable references which will be expanded before the definition is
-made.  @xref{Flavors, ,The Two Flavors of Variables}.
+Variables defined with @samp{=} are @dfn{recursively expanded} variables.
+Variables defined with @samp{:=} or @samp{::=} are @dfn{simply expanded}
+variables; these definitions can contain variable references which will be
+expanded before the definition is made.  Variables defined with @samp{:::=}
+are @dfn{immediately expanded} variables.  The different assignment operators
+are described in @xref{Flavors, ,The Two Flavors of Variables}.
 
 The variable name may contain function and variable references, which
 are expanded when the line is read to find the actual variable name to use.
@@ -5987,13 +6096,12 @@ originally.  @xref{Flavors, ,The Two Flavors of Variables}, for an
 explanation of the two flavors of variables.
 
 When you add to a variable's value with @samp{+=}, @code{make} acts
-essentially as if you had included the extra text in the initial
-definition of the variable.  If you defined it first with @samp{:=} or
-@samp{::=}, making it a simply-expanded variable, @samp{+=} adds to
-that simply-expanded definition, and expands the new text before
-appending it to the old value just as @samp{:=} does (see
-@ref{Setting, ,Setting Variables}, for a full explanation of
-@samp{:=} or @samp{::=}).  In fact,
+essentially as if you had included the extra text in the initial definition of
+the variable.  If you defined it first with @samp{:=} or @samp{::=}, making it
+a simply-expanded variable, @samp{+=} adds to that simply-expanded definition,
+and expands the new text before appending it to the old value just as
+@samp{:=} does (see @ref{Setting, ,Setting Variables}, for a full explanation
+of @samp{:=} or @samp{::=}).  In fact,
 
 @example
 variable := value
@@ -6010,15 +6118,9 @@ variable := $(variable) more
 @end example
 
 On the other hand, when you use @samp{+=} with a variable that you defined
-first to be recursively-expanded using plain @samp{=}, @code{make} does
-something a bit different.  Recall that when you define a
-recursively-expanded variable, @code{make} does not expand the value you set
-for variable and function references immediately.  Instead it stores the text
-verbatim, and saves these variable and function references to be expanded
-later, when you refer to the new variable (@pxref{Flavors, ,The Two Flavors
-of Variables}).  When you use @samp{+=} on a recursively-expanded variable,
-it is this unexpanded text to which @code{make} appends the new text you
-specify.
+first to be recursively-expanded using plain @samp{=} or @samp{:::=},
+@code{make} appends the un-expanded text to the existing value, whatever it
+is.  This means that
 
 @example
 @group
@@ -6247,6 +6349,7 @@ In such situations you may want to use the @code{undefine} directive to
 make a variable appear as if it was never set. For example:
 
 @example
+@group
 foo := foo
 bar = bar
 
@@ -6255,6 +6358,7 @@ undefine bar
 
 $(info $(origin foo))
 $(info $(flavor bar))
+@end group
 @end example
 
 This example will print ``undefined'' for both variables.
@@ -6342,14 +6446,14 @@ variable only.
 Multiple @var{target} values create a target-specific variable value for
 each member of the target list individually.
 
-The @var{variable-assignment} can be any valid form of assignment;
-recursive (@samp{=}), simple (@samp{:=} or @samp{::=}), appending
-(@samp{+=}), or conditional (@samp{?=}).  All variables that appear
-within the @var{variable-assignment} are evaluated within the context
-of the target: thus, any previously-defined target-specific variable
-values will be in effect.  Note that this variable is actually
-distinct from any ``global'' value: the two variables do not have to
-have the same flavor (recursive vs.@: simple).
+The @var{variable-assignment} can be any valid form of assignment; recursive
+(@samp{=}), simple (@samp{:=} or @samp{::=}), immediate (@samp{::=}),
+appending (@samp{+=}), or conditional (@samp{?=}).  All variables that appear
+within the @var{variable-assignment} are evaluated within the context of the
+target: thus, any previously-defined target-specific variable values will be
+in effect.  Note that this variable is actually distinct from any ``global''
+value: the two variables do not have to have the same flavor (recursive vs.@:
+simple).
 
 Target-specific variables have the same priority as any other makefile
 variable.  Variables provided on the command line (and in the
@@ -12362,6 +12466,7 @@ Here is a summary of the directives GNU @code{make} recognizes:
 @itemx define @var{variable} =
 @itemx define @var{variable} :=
 @itemx define @var{variable} ::=
+@itemx define @var{variable} :::=
 @itemx define @var{variable} +=
 @itemx define @var{variable} ?=
 @itemx endef
diff --git a/src/function.c b/src/function.c
index 396f1297..5a7ad3a5 100644
--- a/src/function.c
+++ b/src/function.c
@@ -465,7 +465,6 @@ func_origin (char *o, char **argv, const char *funcname UNUSED)
   else
     switch (v->origin)
       {
-      default:
       case o_invalid:
         abort ();
         break;
diff --git a/src/makeint.h b/src/makeint.h
index 41c90e5b..eb78b402 100644
--- a/src/makeint.h
+++ b/src/makeint.h
@@ -407,7 +407,7 @@ extern int unixy_shell;
 #define NONE_SET(_v,_m) (! ANY_SET ((_v),(_m)))
 
 #define MAP_NUL         0x0001
-#define MAP_BLANK       0x0002
+#define MAP_BLANK       0x0002  /* space, TAB */
 #define MAP_NEWLINE     0x0004
 #define MAP_COMMENT     0x0008
 #define MAP_SEMI        0x0010
diff --git a/src/variable.c b/src/variable.c
index 0d1f1af6..9f66c649 100644
--- a/src/variable.c
+++ b/src/variable.c
@@ -1194,7 +1194,6 @@ do_variable_definition (const floc *flocp, const char *varname,
 
   switch (flavor)
     {
-    default:
     case f_bogus:
       /* Should not be possible.  */
       abort ();
@@ -1205,6 +1204,25 @@ do_variable_definition (const floc *flocp, const char *varname,
          target-specific variable.  */
       p = alloc_value = allocated_variable_expand (value);
       break;
+    case f_expand:
+      {
+        /* A POSIX "var :::= value" assignment.  Expand the value, then it
+           becomes a recursive variable.  After expansion convert all '$'
+           tokens to '$$' to resolve to '$' when recursively expanded.  */
+        char *t = allocated_variable_expand (value);
+        char *np = alloc_value = xmalloc (strlen (t) * 2 + 1);
+        p = t;
+        while (p[0] != '\0')
+          {
+            if (p[0] == '$')
+              *(np++) = '$';
+            *(np++) = *(p++);
+          }
+        *np = '\0';
+        p = alloc_value;
+        free (t);
+        break;
+      }
     case f_shell:
       {
         /* A shell definition "var != value".  Expand value, pass it to
@@ -1440,8 +1458,8 @@ do_variable_definition (const floc *flocp, const char *varname,
      invoked in places where we want to define globally visible variables,
      make sure we define this variable in the global set.  */
 
-  v = define_variable_in_set (varname, strlen (varname), p,
-                              origin, flavor == f_recursive,
+  v = define_variable_in_set (varname, strlen (varname), p, origin,
+                              flavor == f_recursive || flavor == f_expand,
                               (target_var
                                ? current_variable_set_list->set : NULL),
                               flocp);
@@ -1456,7 +1474,7 @@ do_variable_definition (const floc *flocp, const char *varname,
 /* Parse P (a null-terminated string) as a variable definition.
 
    If it is not a variable definition, return NULL and the contents of *VAR
-   are undefined, except NAME is set to the first non-space character or NIL.
+   are undefined, except NAME points to the first non-space character or EOS.
 
    If it is a variable definition, return a pointer to the char after the
    assignment token and set the following fields (only) of *VAR:
@@ -1468,15 +1486,17 @@ do_variable_definition (const floc *flocp, const char *varname,
   */
 
 char *
-parse_variable_definition (const char *p, struct variable *var)
+parse_variable_definition (const char *str, struct variable *var)
 {
-  int wspace = 0;
-  const char *e = NULL;
+  const char *p = str;
+  const char *end = NULL;
 
   NEXT_TOKEN (p);
   var->name = (char *)p;
   var->length = 0;
 
+  /* Walk through STR until we find a valid assignment operator.  Each time
+     through this loop P points to the next character to consider.  */
   while (1)
     {
       int c = *p++;
@@ -1485,26 +1505,112 @@ parse_variable_definition (const char *p, struct variable *var)
       if (STOP_SET (c, MAP_COMMENT|MAP_NUL))
         return NULL;
 
+      if (ISBLANK (c))
+        {
+          /* Variable names can't contain spaces so if this is the second set
+             of spaces we know it's not a variable assignment.  */
+          if (end)
+            return NULL;
+          end = p - 1;
+          NEXT_TOKEN (p);
+          continue;
+        }
+
+      /* If we found = we're done!  */
+      if (c == '=')
+        {
+          if (!end)
+            end = p - 1;
+          var->flavor = f_recursive;
+          break;
+        }
+
+      if (c == ':')
+        {
+          if (!end)
+            end = p - 1;
+
+          /* We need to distinguish :=, ::=, and :::=, and : outside of an
+             assignment (which means this is not a variable definition).  */
+          c = *p++;
+          if (c == '=')
+            {
+              var->flavor = f_simple;
+              break;
+            }
+          if (c == ':')
+            {
+              c = *p++;
+              if (c == '=')
+                {
+                  var->flavor = f_simple;
+                  break;
+                }
+              if (c == ':' && *p++ == '=')
+                {
+                  var->flavor = f_expand;
+                  break;
+                }
+            }
+          return NULL;
+        }
+
+      /* See if it's one of the other two-byte operators.  */
+      if (*p == '=')
+        {
+          switch (c)
+            {
+            case '+':
+              var->flavor = f_append;
+              break;
+            case '?':
+              var->flavor = f_conditional;
+              break;
+            case '!':
+              var->flavor = f_shell;
+              break;
+            default:
+              goto other;
+            }
+
+          if (!end)
+            end = p - 1;
+          ++p;
+          break;
+        }
+
+    other:
+      /* We found a char which is not part of an assignment operator.
+         If we've seen whitespace, then we know this is not a variable
+         assignment since variable names cannot contain whitespace.  */
+      if (end)
+        return NULL;
+
       if (c == '$')
         {
-          /* This begins a variable expansion reference.  Make sure we don't
-             treat chars inside the reference as assignment tokens.  */
+          /* Skip any variable reference, to ensure we don't treat chars
+             inside the reference as assignment operators.  */
           char closeparen;
           unsigned int count;
 
           c = *p++;
-          if (c == '(')
-            closeparen = ')';
-          else if (c == '{')
-            closeparen = '}';
-          else if (c == '\0')
-            return NULL;
-          else
-            /* '$$' or '$X'.  Either way, nothing special to do here.  */
-            continue;
+          switch (c)
+            {
+            case '(':
+              closeparen = ')';
+              break;
+            case '{':
+              closeparen = '}';
+              break;
+            case '\0':
+              return NULL;
+            default:
+              /* '$$' or '$X': skip it.  */
+              continue;
+            }
 
-          /* P now points past the opening paren or brace.
-             Count parens or braces until it is matched.  */
+          /* P now points past the opening paren or brace.  Count parens or
+             braces until we find the closing paren/brace.  */
           for (count = 1; *p != '\0'; ++p)
             {
               if (*p == closeparen && --count == 0)
@@ -1515,82 +1621,12 @@ parse_variable_definition (const char *p, struct variable *var)
               if (*p == c)
                 ++count;
             }
-          continue;
         }
-
-      /* If we find whitespace skip it, and remember we found it.  */
-      if (ISBLANK (c))
-        {
-          wspace = 1;
-          e = p - 1;
-          NEXT_TOKEN (p);
-          c = *p;
-          if (c == '\0')
-            return NULL;
-          ++p;
-        }
-
-
-      if (c == '=')
-        {
-          var->flavor = f_recursive;
-          if (! e)
-            e = p - 1;
-          break;
-        }
-
-      /* Match assignment variants (:=, +=, ?=, !=)  */
-      if (*p == '=')
-        {
-          switch (c)
-            {
-              case ':':
-                var->flavor = f_simple;
-                break;
-              case '+':
-                var->flavor = f_append;
-                break;
-              case '?':
-                var->flavor = f_conditional;
-                break;
-              case '!':
-                var->flavor = f_shell;
-                break;
-              default:
-                /* If we skipped whitespace, non-assignments means no var.  */
-                if (wspace)
-                  return NULL;
-
-                /* Might be assignment, or might be $= or #=.  Check.  */
-                continue;
-            }
-          if (! e)
-            e = p - 1;
-          ++p;
-          break;
-        }
-
-      /* Check for POSIX ::= syntax  */
-      if (c == ':')
-        {
-          /* A colon other than :=/::= is not a variable defn.  */
-          if (*p != ':' || p[1] != '=')
-            return NULL;
-
-          /* POSIX allows ::= to be the same as GNU make's := */
-          var->flavor = f_simple;
-          if (! e)
-            e = p - 1;
-          p += 2;
-          break;
-        }
-
-      /* If we skipped whitespace, non-assignments means no var.  */
-      if (wspace)
-        return NULL;
     }
 
-  var->length = (unsigned int) (e - var->name);
+  /* We found a valid variable assignment: END points to the char after the
+     end of the variable name and P points to the char after the =.  */
+  var->length = (unsigned int) (end - var->name);
   var->value = next_token (p);
   return (char *)p;
 }
@@ -1690,7 +1726,6 @@ print_variable (const void *item, void *arg)
       origin = _("'override' directive");
       break;
     case o_invalid:
-    default:
       abort ();
     }
   fputs ("# ", stdout);
diff --git a/src/variable.h b/src/variable.h
index 509777c4..194b82d5 100644
--- a/src/variable.h
+++ b/src/variable.h
@@ -35,6 +35,7 @@ enum variable_flavor
     f_bogus,            /* Bogus (error) */
     f_simple,           /* Simple definition (:= or ::=) */
     f_recursive,        /* Recursive definition (=) */
+    f_expand,           /* POSIX :::= assignment */
     f_append,           /* Appending definition (+=) */
     f_conditional,      /* Conditional definition (?=) */
     f_shell,            /* Shell assignment (!=) */
diff --git a/tests/scripts/variables/define b/tests/scripts/variables/define
index 984bdd8e..eecbd8f3 100644
--- a/tests/scripts/variables/define
+++ b/tests/scripts/variables/define
@@ -61,7 +61,7 @@ all: ; $(multi)
 
 # TEST 1a: Various new-style define/endef, with no spaces
 
-run_make_test('
+run_make_test(q!
 FOO = foo
 
 define multi=
@@ -77,6 +77,10 @@ define posix::=
 @echo $(FOO)
 endef
 
+define posixbsd:::=
+@echo '$(FOO)$$bar'
+endef
+
 append = @echo a
 
 define append+=
@@ -97,10 +101,11 @@ FOO = there
 all: ; $(multi)
 	$(simple)
 	$(posix)
+	$(posixbsd)
 	$(append)
 	$(cond)
-',
-              '', "echo hi\nhi\nthere\nfoo\nfoo\na\nb\nfirst\n");
+!,
+              '', "echo hi\nhi\nthere\nfoo\nfoo\nfoo\$bar\na\nb\nfirst\n");
 
 # TEST 2: define in true section of conditional (containing conditional)
 
diff --git a/tests/scripts/variables/flavors b/tests/scripts/variables/flavors
index 831e5d81..627672f8 100644
--- a/tests/scripts/variables/flavors
+++ b/tests/scripts/variables/flavors
@@ -153,4 +153,48 @@ dep: ; @: $(info recur=/$(recur)/ simple=/$(simple)/)
 !,
              '', "recur=/onetwothree/ simple=/fourfivesix/\n");
 
+# Test POSIX :::=
+# This creates a recursive variable, but it expands the RHS first.  Any
+# variable escapes ('$$') are preserved so that this recursive variable can be
+# expanded again without changing its contents.
+run_make_test('
+bar = Goodbye
+foo :::= $(bar)
+bar = ${ugh}
+ugh = Hello
+all: ; @echo $(foo)
+',
+              '', "Goodbye");
+
+# POSIX :::= no spaces
+run_make_test(q!
+bar = Goodbye
+foo:::=$(bar)
+bar = ${ugh}
+ugh = Hello
+all: ; @echo $(foo)
+!,
+              '', "Goodbye");
+
+# Variable escapes ('$$') are preserved.
+run_make_test(q!
+bar = Good$$bye
+foo :::= $(bar) $$what
+bar = ${ugh}
+ugh = Hello
+all: ; @echo '$(foo)'
+!,
+              '', 'Good$bye $what');
+
+# Append works as expected
+run_make_test(q!
+bar = Good$$bye
+foo :::= $(bar)
+foo += $$what $(bar)
+bar = ${ugh}
+ugh = Hello
+all: ; @echo '$(foo)'
+!,
+              '', 'Good$bye $what Hello');
+
 1;
diff --git a/tests/scripts/variables/negative b/tests/scripts/variables/negative
index 0f9abc82..5cb600a8 100644
--- a/tests/scripts/variables/negative
+++ b/tests/scripts/variables/negative
@@ -43,4 +43,18 @@ run_make_test(undef,
               '#MAKEFILE#:4: *** unterminated variable reference.  Stop.',
               512);
 
+# Whitespace not allowed in variable names
+run_make_test('x y =', '',
+              '#MAKEFILE#:1: *** missing separator.  Stop.', 512);
+
+run_make_test('x y=', '',
+              '#MAKEFILE#:1: *** missing separator.  Stop.', 512);
+
+# In theory an empty variable should be ignored, but during parsing it's a
+# real token and so this fails.  I'm not 100% sure if this is right or not.
+
+run_make_test('x $X=', '',
+              '#MAKEFILE#:1: *** missing separator.  Stop.', 512);
+
+
 1;