Significant reworking of how variables are parsed and stored. They are now stored as strings, instead of Expansion objects. Recursively-expanded variables are parsed at the time they are expanded. This coupling between the data model and the parser is ridiculous, but compatible.
authorBenjamin Smedberg <benjamin@smedbergs.us>
Mon, 09 Feb 2009 16:45:07 -0500
changeset 67 63531e755f52
parent 66 cbeb0eba9087
child 68 6e86f2c1921a
push id39
push userbsmedberg@mozilla.com
push date2009-02-09 21:45 +0000
Significant reworking of how variables are parsed and stored. They are now stored as strings, instead of Expansion objects. Recursively-expanded variables are parsed at the time they are expanded. This coupling between the data model and the parser is ridiculous, but compatible.
pymake/data.py
pymake/parser.py
tests/parsertests.py
tests/unterminated-dollar.mk
--- a/pymake/data.py
+++ b/pymake/data.py
@@ -157,40 +157,48 @@ class Variables(object):
     SOURCE_AUTOMATIC = 4
     # I have no intention of supporting builtin rules or variables that go with them
     # SOURCE_IMPLICIT = 5
 
     def __init__(self, parent=None):
         self._map = {}
         self.parent = parent
 
-    def get(self, name):
+    def get(self, name, expand=True):
         """
         Get the value of a named variable. Returns a tuple (flavor, source, value)
 
         If the variable is not present, returns (None, None, None)
+
+        @param expand If true, the value will be returned as an expansion. If false,
+        it will be returned as an unexpanded string.
         """
-        v = self._map.get(name, None)
-        if v is not None:
-            return v
+        if name in self._map:
+            flavor, source, valuestr = self._map[name]
+            if not expand:
+                return flavor, source, valuestr
+
+            if flavor == self.FLAVOR_RECURSIVE:
+                d = pymake.parser.Data(None, None)
+                d.append(valuestr, pymake.parser.Location("Expansion of variable '%s'" % (name,), 1, 0))
+                val, t, o = pymake.parser.parsemakesyntax(d, 0, (), pymake.parser.iterdata)
+            else:
+                val = Expansion.fromstring(valuestr)
+
+            return flavor, source, val
 
         if self.parent is not None:
-            return self.parent.get(name)
+            return self.parent.get(name, expand)
 
         return (None, None, None)
 
     def set(self, name, flavor, source, value):
-        if not flavor in (self.FLAVOR_RECURSIVE, self.FLAVOR_SIMPLE):
-            raise DataError("Unexpected variable flavor: %s" % (flavor,))
-
-        if not source in (self.SOURCE_OVERRIDE, self.SOURCE_MAKEFILE, self.SOURCE_AUTOMATIC):
-            raise DataError("Unexpected variable source: %s" % (source,))
-
-        if not isinstance(value, Expansion):
-            raise DataError("Unexpected variable value, wasn't an expansion.")
+        assert flavor in (self.FLAVOR_RECURSIVE, self.FLAVOR_SIMPLE)
+        assert source in (self.SOURCE_OVERRIDE, self.SOURCE_MAKEFILE, self.SOURCE_AUTOMATIC)
+        assert isinstance(value, str)
 
         prevflavor, prevsource, prevvalue = self.get(name)
         if prevsource is not None and source > prevsource:
             # TODO: give a location for this warning
             log.warning("not setting variable '%s', set by higher-priority source to value '%s'" % (name, prevvalue))
             return
 
         self._map[name] = (flavor, source, value)
@@ -551,26 +559,26 @@ class Target(object):
             if commandrule is not None and remake:
                 self.remake()
                 commandrule.execute(self, makefile)
 
 def setautomaticvariables(v, makefile, target, prerequisites):
     vprereqs = [makefile.gettarget(p).vpathtarget
                 for p in prerequisites]
 
-    v.set('@', Variables.FLAVOR_SIMPLE, Variables.SOURCE_AUTOMATIC, Expansion.fromstring(target.vpathtarget))
+    v.set('@', Variables.FLAVOR_SIMPLE, Variables.SOURCE_AUTOMATIC, target.vpathtarget)
 
     if len(vprereqs):
-        v.set('<', Variables.FLAVOR_SIMPLE, Variables.SOURCE_AUTOMATIC, Expansion.fromstring(vprereqs[0]))
+        v.set('<', Variables.FLAVOR_SIMPLE, Variables.SOURCE_AUTOMATIC, vprereqs[0])
 
     # TODO '?'
     v.set('^', Variables.FLAVOR_SIMPLE, Variables.SOURCE_AUTOMATIC,
-          Expansion.fromstring(' '.join(withoutdups(vprereqs))))
+          ' '.join(withoutdups(vprereqs)))
     v.set('+', Variables.FLAVOR_SIMPLE, Variables.SOURCE_AUTOMATIC,
-          Expansion.fromstring(' '.join(vprereqs)))
+          ' '.join(vprereqs))
     # TODO '|'
     # TODO all the D and F variants
 
 def splitcommand(command):
     """
     Using the esoteric rules, split command lines by unescaped newlines.
     """
     start = 0
@@ -679,18 +687,17 @@ class PatternRule(object):
     def execute(self, target, makefile):
         assert isinstance(target, Target)
 
         dir, stem = self.matchfor(target.target)
 
         v = Variables(parent=target.variables)
         setautomaticvariables(v, makefile, target,
                               self.prerequisitesfor(stem=stem, dir=dir))
-        v.set('*', Variables.FLAVOR_SIMPLE, Variables.SOURCE_AUTOMATIC,
-              Expansion.fromstring(stem))
+        v.set('*', Variables.FLAVOR_SIMPLE, Variables.SOURCE_AUTOMATIC, stem)
 
         for c in self.commands:
             cstring = c.resolve(v, None)
             if cstring[0:1] == '@':
                 cstring = cstring[1:]
             subprocess.check_call(cstring, shell=True)
 
 class Makefile(object):
--- a/pymake/parser.py
+++ b/pymake/parser.py
@@ -15,16 +15,17 @@ Lines with an initial tab are commands i
 Otherwise, they are parsed as makefile syntax.
 
 After splitting data into parseable chunks, we use a recursive-descent parser to
 nest parenthesized syntax.
 """
 
 import logging
 from pymake import data, functions
+from cStringIO import StringIO
 
 tabwidth = 4
 
 log = logging.getLogger('pymake.parser')
 
 class SyntaxError(Exception):
     def __init__(self, message, loc):
         self.message = message
@@ -98,18 +99,19 @@ class Data(object):
 
     def __len__(self):
         return len(self.data)
 
     def readline(self):
         try:
             lineno, line = self.lineiter.next()
             self.append(line, Location(self.path, lineno, 0))
+            return True
         except StopIteration:
-            pass
+            return False
 
     def __getitem__(self, key):
         try:
             return self.data[key]
         except IndexError:
             return None
 
     def append(self, data, loc):
@@ -132,29 +134,32 @@ class Data(object):
         """
         while offset < len(self.data):
             c = self.data[offset]
             if not c.isspace():
                 break
             offset += 1
         return offset
 
-    def findtoken(self, o, tlist):
+    def findtoken(self, o, tlist, needws):
         """
         Check data at position o for any of the tokens in tlist followed by whitespace
         or end-of-data.
 
         If a token is found, skip trailing whitespace and return (token, newoffset).
         Otherwise return None, oldoffset
         """
         for t in tlist:
             end = o + len(t)
-            if self.data[o:end] == t and (end == len(self.data) or self.data[end].isspace()):
-                end = self.skipwhitespace(end)
-                return t, end
+            if self.data[o:end] == t:
+                if not needws:
+                    return t, end
+                elif end == len(self.data) or self.data[end].isspace():
+                    end = self.skipwhitespace(end)
+                    return t, end
         return None, o
 
 def iterdata(d, offset):
     """
     A Data iterator yielding (char, offset, location) without any escaping.
     """
     while offset < len(d.data):
         yield d.data[offset], offset, d.getloc(offset)
@@ -193,16 +198,21 @@ def itermakefilechars(d, offset):
                 # see escape-chars.mk VARAWFUL
                 offset += 1
                 yield '\\', offset, d.getloc(offset)
                 offset += 1
             elif c2 == '\n':
                 yield ' ', offset, d.getloc(offset)
                 d.readline()
                 offset = d.skipwhitespace(offset + 2)
+            elif c2 == '\\':
+                yield '\\', offset, d.getloc(offset)
+                offset += 1
+                yield '\\', offset, d.getloc(offset)
+                offset += 1
             else:
                 yield c, offset, d.getloc(offset)
                 offset += 1
         else:
             if c.isspace():
                 o = d.skipwhitespace(offset)
                 if d.data[o:o+2] == '\\\n':
                     offset = o
@@ -254,17 +264,17 @@ def iterdefinechars(d, offset):
         """
         if o >= len(d.data):
             return 0
 
         if d.data[o] == '\t':
             return 0
 
         o = d.skipwhitespace(o)
-        token, o = d.findtoken(o, ('define', 'endef'))
+        token, o = d.findtoken(o, ('define', 'endef'), True)
         if token == 'define':
             return 1
 
         if token == 'endef':
             return -1
         
         return 0
 
@@ -277,144 +287,162 @@ def iterdefinechars(d, offset):
         c = d.data[offset]
 
         if c == '\n':
             d.readline()
             definecount += checkfortoken(offset + 1)
             if definecount == 0:
                 return
 
+        if c == '\\' and offset < len(d.data) - 1 and d.data[offset+1] == '\n':
+            yield ' ', offset, d.getloc(offset)
+            d.readline()
+            offset = d.skipwhitespace(offset + 2)
+            continue
+
+        if c.isspace():
+            o = d.skipwhitespace(offset)
+            if d.data[o:o+2] == '\\\n':
+                offset = o
+                continue
+
         yield c, offset, d.getloc(offset)
         offset += 1
 
-        if c == '\\' and offset < len(d.data) and d.data[offset] == '\n':
-            yield '\n', offset, d.getloc(offset)
-            d.readline()
-            offset += 1
 
     # Unlike the other iterators, if you fall off this one there is an unterminated
     # define.
     raise SyntaxError("Unterminated define", d.getloc(startoffset))
 
+def ensureend(d, offset, msg, ifunc=itermakefilechars):
+    """
+    Ensure that only whitespace remains in this data.
+    """
+
+    for c, o, l in ifunc(d, offset):
+        if not c.isspace():
+            raise SyntaxError(msg, d.getloc(offset))
+
 def iterlines(fd):
     """Yield (lineno, line) for each line in fd"""
 
     lineno = 0
     for line in fd:
         lineno += 1
 
         if line.endswith('\r\n'):
             line = line[:-2] + '\n'
 
         yield (lineno, line)
 
-def getkeyword(d, offset):
+def setvariable(resolvevariables, setvariables, vname, token, d, offset, iterfunc=itermakefilechars, fromcl=False):
     """
-    Look through d at offset for a potential keyword made up of only a-z and hyphen
-    and followed by a white space. Return word, newoffset or None, None
+    Parse what's left in a data iterator di into a variable.
     """
-    i = offset
-    while True:
-        c = d[i]
-        if c == '-' or (c >= 'a' and c <= 'z'):
-            i += 1
-            continue
-        if i > offset and (c is None or c.isspace()):
-            return d[offset:i], d.skipwhitespace(i)
-        return None, i
+    assert isinstance(resolvevariables, data.Variables)
+    assert isinstance(setvariables, data.Variables)
 
-def setvariable(variables, vname, recursive, value, fromcl=False):
-    """
-    Parse the remaining data at d[offset] into a variables object.
+    # print "setvariable: %r resvariables: %r setvariables: %r" % (vname, resolvevariables, setvariables)
 
-    @param vname an string holding the variable name
-    """
     if len(vname) == 0:
         raise SyntaxError("Empty variable name", loc=d.getloc(offset))
 
     if fromcl:
         source = data.Variables.SOURCE_OVERRIDE
     else:
         source = data.Variables.SOURCE_MAKEFILE
 
-    if recursive:
+    if token == '+=':
+        raise NotImplementedError("+= not yet")
+    elif token == '=':
         flavor = data.Variables.FLAVOR_RECURSIVE
+        val = ''.join((c for c, o, l in iterfunc(d, offset)))
     else:
+        assert token == ':='
+
         flavor = data.Variables.FLAVOR_SIMPLE
-        e = data.Expansion()
-        e.append(value.resolve(variables, vname))
-        value = e
+        e, t, o = parsemakesyntax(d, offset, (), itermakefilechars)
+        val = e.resolve(resolvevariables, vname)
         
-    variables.set(vname, flavor, source, value)
+    setvariables.set(vname, flavor, source, val)
 
 def parsecommandlineargs(makefile, args):
     """
     Given a set of arguments from a command-line invocation of make,
     parse out the variable definitions and return the rest as targets.
     """
 
     r = []
-    for a in args:
-        eqpos = a.find('=')
-        if eqpos != -1:
-            if a[eqpos-1] == ':':
-                vname = a[:eqpos-1]
-            else:
-                vname = a[:eqpos]
+    for i in xrange(0, len(args)):
+        a = args[i]
+
+        vname, t, val = a.partition(':=')
+        if t == '':
+            vname, t, val = a.partition('=')
+        if t != '':
             vname = vname.strip()
-            valtext = a[eqpos+1:].lstrip()
             d = Data(None, None)
-            d.append(valtext, Location('<command-line>', 1, eqpos + 1))
-            value, offset = parsemakesyntax(d, 0, '', PARSESTYLE_MAKEFILE)
-            assert offset == -1
-            setvariable(makefile.variables, vname, a[eqpos-1] != ':', value, True)
+            d.append(val, Location('<command-line>', i, len(vname) + len(t)))
+
+            setvariable(makefile.variables, makefile.variables,
+                        vname, t, d, 0, fromcl=True,
+                        iterfunc=iterdata)
         else:
             r.append(a)
 
     return r
 
 def ifeq(d, offset, makefile):
     # the variety of formats for this directive is rather maddening
-    if d[offset] == '(':
-        arg1, offset = parsemakesyntax(d, offset + 1, ',', PARSESTYLE_MAKEFILE)
-        if offset == -1:
-            raise SyntaxError("Unexpected text in conditional", d.getloc(len(d) - 1))
-        offset = d.skipwhitespace(offset + 1)
-        arg2, offset = parsemakesyntax(d, offset, ')', PARSESTYLE_MAKEFILE)
-        if offset == -1:
-            raise SyntaxError("Unexpected text in conditional", d.getloc(len(d) - 1))
-        offset = d.skipwhitespace(offset + 1)
-        if d[offset] not in ('#', None):
-            raise SyntaxError("Unexpected text after conditional", d.getloc(offset))
-    elif d[offset] in '\'"':
-        arg1, offset = parsemakesyntax(d, offset + 1, d[offset], PARSESTYLE_MAKEFILE)
-        if offset == -1:
-            raise SyntaxError("Unexpected text in conditional", d.getloc(len(d) - 1))
-        offset = d.skipwhitespace(offset + 1)
-        if d[offset] not in '\'"':
+    token, offset = d.findtoken(offset, ('(', "'", '"'), False)
+    if token is None:
+        raise SyntaxError("No arguments after conditional", d.getloc(offset))
+
+    if token == '(':
+        arg1, t, offset = parsemakesyntax(d, offset, (',',), itermakefilechars)
+        if t is None:
+            raise SyntaxError("Expected two arguments in conditional", d.getloc(offset))
+
+        offset = d.skipwhitespace(offset)
+        arg2, t, offset = parsemakesyntax(d, offset, (')',), itermakefilechars)
+        if t is None:
             raise SyntaxError("Unexpected text in conditional", d.getloc(offset))
-        arg2, offset = parsemakesyntax(d, offset + 1, d[offset], PARSESTYLE_MAKEFILE)
-        offset = d.skipwhitespace(offset + 1)
-        if d[offset] not in ('#', None):
-            raise SyntaxError("Unexpected text after conditional: %c" % (d[offset],), d.getloc(offset))
+
+        ensureend(d, offset, "Unexpected text after conditional")
+    else:
+        arg1, t, offset = parsemakesyntax(d, offset, (token,), itermakefilechars)
+        if t is None:
+            raise SyntaxError("Unexpected text in conditional", d.getloc(offset))
+
+        offset = d.skipwhitespace(offset)
+        if offset == len(d):
+            raise SyntaxError("Expected two arguments in conditional", d.getloc(offset))
+
+        token = d[offset]
+        if token not in '\'"':
+            raise SyntaxError("Unexpected text in conditional", d.getloc(offset))
+
+        arg2, t, offset = parsemakesyntax(d, offset + 1, (token,), itermakefilechars)
+
+        ensureend(d, offset, "Unexpected text after conditional")
 
     val1 = arg1.resolve(makefile.variables, None)
     val2 = arg2.resolve(makefile.variables, None)
     return val1 == val2
 
 def ifneq(d, offset, makefile):
     return not ifeq(d, offset, makefile)
 
 def ifdef(d, offset, makefile):
-    e, offset = parsemakesyntax(d, offset, '', PARSESTYLE_MAKEFILE)
+    e, t, offset = parsemakesyntax(d, offset, (), itermakefilechars)
     e.rstrip()
 
     vname = e.resolve(makefile.variables, None)
 
-    flavor, source, value = makefile.variables.get(vname)
+    flavor, source, value = makefile.variables.get(vname, expand=False)
 
     if value is None:
         return False
 
     # We aren't expanding the variable... we're just seeing if it was set to a non-empty
     # expansion.
     return len(value) > 0
 
@@ -442,213 +470,213 @@ class Condition(object):
         if self.everactive:
             self.active = False
             return
 
         self.active = active
         if active:
             self.everactive = True
 
+directives = [k for k in conditionkeywords.iterkeys()] + \
+    ['else', 'endif', 'define', 'endef', 'override', 'include', 'vpath']
+
+varsettokens = (':=', '+=', '=')
+
 def parsestream(fd, filename, makefile):
     """
     Parse a stream of makefile into a makefile data structure.
 
     @param fd A file-like object containing the makefile data.
     """
 
     currule = None
     condstack = []
 
     fdlines = iterlines(fd)
 
-    for lineno, line in fdlines:
+    while True:
         d = Data(fdlines, filename)
-        if line.startswith('\t') and currule is not None:
+        if not d.readline():
+            break
+
+        if len(d.data) > 0 and d.data[0] == '\t' and currule is not None:
             if any((not c.active for c in condstack)):
-                log.info('skipping line %i, ifdefed away' % lineno)
+                log.info('%s: skipping line because of active conditions' % (d.getloc(0),))
                 continue
 
-            d.append(line[1:], Location(filename, lineno, tabwidth))
-            e, stoppedat = parsemakesyntax(d, 0, '', PARSESTYLE_COMMAND)
-            assert stoppedat == -1
+            e, t, o = parsemakesyntax(d, 1, (), itercommandchars)
+            assert t == None
             currule.addcommand(e)
         else:
             # To parse Makefile syntax, we first strip leading whitespace and
             # look for initial keywords. If there are no keywords, it's either
             # setting a variable or writing a rule.
 
-            d = Data(fdlines, filename)
-            d.append(line, Location(filename, lineno, 0))
-
             offset = d.skipwhitespace(0)
 
-            kword, kwoffset = getkeyword(d, offset)
+            kword, offset = d.findtoken(offset, directives, True)
             if kword == 'endif':
-                if d[kwoffset] not in ('#', None):
-                    raise SyntaxError("Unexpected data after 'endif' directive.",
-                                      d.getloc(kwoffset))
-
+                ensureend(d, offset, "Unexpected data after 'endif' directive")
                 if not len(condstack):
                     raise SyntaxError("unmatched 'endif' directive",
                                       d.getloc(offset))
 
                 condstack.pop()
                 continue
-            elif kword == 'else':
+            
+            if kword == 'else':
                 if not len(condstack):
                     raise SyntaxError("unmatched 'else' directive",
                                       d.getloc(offset))
 
-                kword, kwoffset = getkeyword(d, kwoffset)
+                kword, offset = d.findtoken(offset, conditionkeywords, True)
                 if kword is None:
-                    if d[kwoffset] not in ('#', None):
-                        raise SyntaxError("Unexpected data after 'else' directive.",
-                                          d.getloc(kwoffset))
-
+                    ensureend(d, offset, "Unexpected data after 'else' directive.")
                     condstack[-1].makeactive(True)
                 else:
                     if kword not in conditionkeywords:
                         raise SyntaxError("Unexpected condition after 'else' directive.",
-                                          d.getloc(kwoffset))
+                                          d.getloc(offset))
                         
-                    m = conditionkeywords[kword](d, kwoffset, makefile)
+                    m = conditionkeywords[kword](d, offset, makefile)
                     condstack[-1].makeactive(m)
                 continue
-            elif kword == 'endef':
-                raise SyntaxError("Extraneous endef", d.getloc(0))
-            elif kword == 'override':
+
+            if kword == 'endef':
+                raise SyntaxError("Unmatched endef", d.getloc(offset))
+
+            if kword == 'override':
                 raise NotImplementedError('no overrides yet')
-            elif kword == 'define':
-                e, i = parsemakesyntax(d, kwoffset, '', PARSESTYLE_MAKEFILE)
-                i = len(d)
+
+            if kword == 'define':
+                e, t, i = parsemakesyntax(d, offset, (), itermakefilechars)
+
+                d = Data(fdlines, filename)
                 d.readline()
-                val, i = parsemakesyntax(d, i, '', PARSESTYLE_DEFINE)
 
-                vname = e.resolve(makefile.variables, None)
-                makefile.variables.set(vname, data.Variables.FLAVOR_RECURSIVE,
-                                       data.Variables.SOURCE_MAKEFILE, val)
+                setvariable(makefile.variables, makefile.variables,
+                            e.resolve(makefile.variables, None),
+                            '=', d, 0, iterdefinechars)
+
                 continue
-            elif kword == 'include':
+
+            if kword == 'include':
                 raise NotImplementedError('no includes yet')
-            elif kword in conditionkeywords:
-                m = conditionkeywords[kword](d, kwoffset, makefile)
+
+            if kword in conditionkeywords:
+                m = conditionkeywords[kword](d, offset, makefile)
                 condstack.append(Condition(m, d.getloc(offset)))
                 continue
 
+            assert kword is None
+
             if any((not c.active for c in condstack)):
-                log.info('skipping line %i, ifdefed away' % lineno)
+                log.info('%s: skipping line because of active conditions' % (d.getloc(0),))
                 continue
 
-            e, stoppedat = parsemakesyntax(d, 0, ':=', PARSESTYLE_MAKEFILE)
-            if stoppedat == -1:
+            e, token, offset = parsemakesyntax(d, offset, varsettokens + ('::', ':'), itermakefilechars)
+            if token is None:
                 v = e.resolve(makefile.variables, None)
                 if v.strip() != '':
                     raise SyntaxError("Bad syntax: non-empty line is not a variable assignment or rule.", loc=d.getloc(0))
                 continue
 
             # if we encountered real makefile syntax, the current rule is over
             currule = None
 
-            if d[stoppedat] == '=' or d[stoppedat:stoppedat+2] == ':=':
-                isrecursive = d[stoppedat] == '='
-
+            if token in varsettokens:
                 e.lstrip()
                 e.rstrip()
                 vname = e.resolve(makefile.variables, None)
-                value, stoppedat = parsemakesyntax(d, stoppedat + (isrecursive and 1 or 2), '', PARSESTYLE_MAKEFILE)
-                assert stoppedat == -1
-                value.lstrip()
-                setvariable(makefile.variables, vname, isrecursive, value)
+
+                offset = d.skipwhitespace(offset)
+
+                setvariable(makefile.variables, makefile.variables,
+                            vname, token, d, offset)
             else:
-                assert d[stoppedat] == ':'
-
-                if d[stoppedat+1] == ':':
-                    doublecolon = True
-                    stoppedat += 1
-                else:
-                    doublecolon = False
+                doublecolon = token == '::'
 
                 # `e` is targets or target patterns, which can end up as
                 # * a rule
                 # * an implicit rule
                 # * a static pattern rule
                 # * a target-specific variable definition
                 # * a pattern-specific variable definition
                 # any of the rules may have order-only prerequisites
                 # delimited by |, and a command delimited by ;
                 targets = map(data.Pattern, data.splitwords(e.resolve(makefile.variables, None)))
                 if len(targets) == 0:
-                    raise SyntaxError("No targets in rule", g.getloc(0))
+                    raise SyntaxError("No targets in rule", g.getloc(offset))
 
                 ispatterns = set((t.ispattern() for t in targets))
                 if len(ispatterns) == 2:
-                    raise SyntaxError("Mixed implicit and normal rule", d.getloc(0))
+                    raise SyntaxError("Mixed implicit and normal rule", d.getloc(offset))
                 ispattern, = ispatterns
 
-                stoppedat += 1
-                e, stoppedat = parsemakesyntax(d, stoppedat, ':=|;', PARSESTYLE_MAKEFILE)
-                if stoppedat == -1 or d[stoppedat] == ';':
+                e, token, offset = parsemakesyntax(d, offset,
+                                                   varsettokens + (':', '|', ';'),
+                                                   itermakefilechars)
+                if token in (None, ';'):
                     prereqs = data.splitwords(e.resolve(makefile.variables, None))
                     if ispattern:
                         currule = data.PatternRule(targets, map(data.Pattern, prereqs), doublecolon, loc=d.getloc(0))
                         makefile.appendimplicitrule(currule)
                     else:
                         currule = data.Rule(prereqs, doublecolon, loc=d.getloc(0))
                         for t in targets:
                             makefile.gettarget(t.gettarget()).addrule(currule)
                         makefile.foundtarget(targets[0].gettarget())
 
-                    if stoppedat != -1:
-                        e, stoppedat = parsemakesyntax(d, stoppedat + 1, '', PARSESTYLE_COMMAND)
-                        assert stoppedat == -1
-                        e.lstrip()
+                    if token == ';':
+                        offset = d.skipwhitespace(offset)
+                        e, t, offset = parsemakesyntax(d, offset, (), itercommandchars)
                         currule.addcommand(e)
-                elif d[stoppedat] == '=' or d[stoppedat:stoppedat+2] == ':=':
-                    isrecursive = d[stoppedat] == '='
+                elif token in varsettokens:
                     e.lstrip()
                     e.rstrip()
                     vname = e.resolve(makefile.variables, None)
-                    value, stoppedat = parsemakesyntax(d, stoppedat + (isrecursive and 1 or 2), '', PARSESTYLE_MAKEFILE)
-                    assert stoppedat == -1
-                    value.lstrip()
 
+                    offset = d.skipwhitespace(offset)
                     if ispattern:
                         for target in targets:
-                            setvariable(makefile.getpatternvariables(target), vname, isrecursive, value)
+                            setvariable(makefile.variables,
+                                        makefile.getpatternvariables(target), vname,
+                                        token, d, offset)
                     else:
                         for target in targets:
-                            setvariable(makefile.gettarget(target.gettarget()).variables, vname, isrecursive, value)
-                elif d[stoppedat] == '|':
+                            setvariable(makefile.variables,
+                                        makefile.gettarget(target.gettarget()).variables,
+                                        vname, token, d, offset)
+                elif token == '|':
                     raise NotImplementedError('order-only prerequisites not implemented')
                 else:
-                    assert d[stoppedat] == ':'
+                    assert token == ':'
 
                     # static pattern rule
                     if ispattern:
                         raise SyntaxError("static pattern rules must have static targets")
 
                     patstr = e.resolve(makefile.variables, None)
                     patterns = data.splitwords(patstr)
                     if len(patterns) != 1:
-                        raise SyntaxError("A static pattern rule may have only one pattern", d.getloc(stoppedat))
+                        raise SyntaxError("A static pattern rule may have only one pattern", d.getloc(offset))
 
                     pattern = data.Pattern(patterns[0])
 
-                    e, stoppedat = parsemakesyntax(d, stoppedat + 1, ';', PARSESTYLE_MAKEFILE)
+                    e, token, offset = parsemakesyntax(d, offset, (';',), itermakefilechars)
                     prereqs = map(data.Pattern, data.splitwords(e.resolve(makefile.variables, None)))
                     currule = data.PatternRule([pattern], prereqs, doublecolon, loc=d.getloc(0))
                     for t in targets:
                         makefile.gettarget(t.gettarget()).addrule(currule)
 
                     makefile.foundtarget(targets[0].gettarget())
 
-                    if stoppedat != -1:
-                        e, stoppedat = parsemakesyntax(d, stoppedat + 1, '', PARSESTYLE_COMMAND)
-                        assert stoppedat == -1
-                        e.lstrip()
+                    if token == ';':
+                        offset = d.skipwhitespace(offset)
+                        e, token, offset = parsemakesyntax(d, offset, (), itercommandchars)
                         currule.addcommand(e)
 
     if len(condstack):
         raise SyntaxError("Condition never terminated with endif", condstack[-1].loc)
 
 PARSESTATE_TOPLEVEL = 0    # at the top level
 PARSESTATE_FUNCTION = 1    # expanding a function call. data is function
 
@@ -660,219 +688,146 @@ PARSESTATE_SUBSTTO = 4     # expanding a
 class ParseStackFrame(object):
     def __init__(self, parsestate, expansion, stopon, **kwargs):
         self.parsestate = parsestate
         self.expansion = expansion
         self.stopon = stopon
         for key, value in kwargs.iteritems():
             setattr(self, key, value)
 
-PARSESTYLE_COMMAND = 0
-PARSESTYLE_MAKEFILE = 1
-PARSESTYLE_DEFINE = 2
+functiontokens = [k for k in functions.functionmap.iterkeys()]
 
-def parsemakesyntax(d, startat, stopon, parsestyle):
+def parsemakesyntax(d, startat, stopon, iterfunc):
     """
     Given Data, parse it into a data.Expansion.
 
     @param stopon (sequence)
         Indicate characters where toplevel parsing should stop.
+
+    @param iterfunc (generator function)
+        A function which is used to iterate over d, yielding (char, offset, loc)
+        @see iterdata
+        @see itermakefilechars
+        @see itercommandchars
  
-    @return a tuple (expansion, stopoffset). If all the data is consumed, stopoffset will be -1
+    @return a tuple (expansion, token, offset). If all the data is consumed,
+    token and offset will be None
     """
 
-    assert parsestyle in (PARSESTYLE_COMMAND, PARSESTYLE_MAKEFILE, PARSESTYLE_DEFINE)
+    # print "parsemakesyntax(%r)" % d.data
+
+    assert callable(iterfunc)
 
     stack = [
         ParseStackFrame(PARSESTATE_TOPLEVEL, data.Expansion(), stopon)
     ]
 
-    if parsestyle == PARSESTYLE_DEFINE:
-        definecount = 1
-        linebegin = True
+    di = iterfunc(d, startat)
+    offset = startat
 
-    i = startat
-    while i < len(d):
+    while True: # this is not a for loop because `di` changes during the function
         stacktop = stack[-1]
-        c = d[i]
+        try:
+            c, offset, loc = di.next()
+        except StopIteration:
+            break
 
-        # print "i=%i c=%c parsestate=%i len(d)=%i" % (i, c, stacktop.parsestate, len(d))
+        # print "  %i: stacklen=%i parsestate=%s looking for %r" % (offset, len(stack),
+        #                                                           stacktop.parsestate, stacktop.stopon),
 
-        if parsestyle == PARSESTYLE_DEFINE and linebegin:
-            linebegin = False
-            if d[i] != '\t':
-                # look for endef/define
-                j = d.skipwhitespace(i)
-                kword, j = getkeyword(d, j)
-                if kword == 'define':
-                    print "incrementing definecount at %s" % d.getloc(j)
-                    definecount += 1
-                elif kword == 'endef':
-                    if not d[j] in ('#', None):
-                        raise SyntaxError("Extraneous text after endef directive.",
-                                          d.getloc(j))
+        token, offset = d.findtoken(offset, stacktop.stopon, False)
+        if token is not None:
+            c = 'dangerwillrobinson!'
+            di = iterfunc(d, offset)
 
-
-                    print "decrementing definecount at %s" % d.getloc(j)
-                    definecount -= 1
-                    if definecount == 0:
-                        stacktop.expansion.trimlastnewline()
-                        break
-
-        if c == '#' and parsestyle == PARSESTYLE_MAKEFILE:
-            # we need to keep reading lines until there are no more continuations
-            while i < len(d):
-                if d[i] == '\\':
-                    if d[i+1] == '\\':
-                        i += 2
-                        continue
-                    elif d[i+1] == '\n':
-                        i += 2
-                        assert i == len(d)
-                        d.readline()
-                elif d[i] == '\n':
-                    i += 1
-                    assert i == len(d)
-                    break
-                i += 1
-            break
-        elif c == '\\':
-            # in makefile syntax, backslashes can escape # specially, but nothing else. Fun, huh?
-            if d[i+1] is None:
-                stacktop.expansion.append('\\')
-                i += 1
-                break
-            elif d[i+1] == '#' and parsestyle == PARSESTYLE_MAKEFILE:
-                stacktop.expansion.append('#')
-                i += 2
-                continue
-            elif d[i+1:i+3] == '\\#':
-                # This is an edge case that I discovered. It's undocumented, and
-                # totally absolutely weird. See escape-chars.mk VARAWFUL
-                stacktop.expansion.append('\\')
-                i += 2
-                continue
-            elif d[i+1] == '\\' and parsestyle == PARSESTYLE_MAKEFILE:
-                stacktop.expansion.append('\\\\')
-                i += 2
-                continue
-            elif d[i+1] == '\n':
-                i += 2
-                assert i == len(d), "newline isn't last character?"
-
-                d.readline()
-
-                if parsestyle == PARSESTYLE_COMMAND:
-                    stacktop.expansion.append('\\\n')
-                    if d[i] == '\t':
-                        i += 1
-                else:
-                    stacktop.expansion.rstrip()
-                    stacktop.expansion.append(' ')
-                    i = d.skipwhitespace(i)
-                continue
-            else:
-                stacktop.expansion.append(c)
-                i += 1
-                continue
-        elif c == '\n':
-            assert i + 1 == len(d), "newline isn't last character? i = %i d = %r" % (i, d.data)
-            if parsestyle == PARSESTYLE_DEFINE:
-                stacktop.expansion.append(c)
-                i += 1
-
-                d.readline()
-                linebegin = True
-                continue
-            else:
-                i += 1
-                break
-        elif c == '$':
-            loc = d.getloc(i)
-            i += 1
-            c = d[i]
-
-            if c == '$':
-                stacktop.expansion.append('$')
-            elif c == '(':
-                # look forward for a function name
-                fname, j = getkeyword(d, i + 1)
-                if fname is not None and fname in functions.functionmap:
-                    fn = functions.functionmap[fname](loc)
-                    stack.append(ParseStackFrame(PARSESTATE_FUNCTION,
-                                                 data.Expansion(), ',)',
-                                                 function=fn))
-                    i = j
-                    continue
-                else:
-                    e = data.Expansion()
-                    stack.append(ParseStackFrame(PARSESTATE_VARNAME, e, ':)', loc=loc))
-            else:
-                fe = data.Expansion()
-                fe.append(d[i])
-                stacktop.expansion.append(functions.VariableRef(loc, fe))
-                i += 1
-                continue
-        elif c in stacktop.stopon:
             if stacktop.parsestate == PARSESTATE_TOPLEVEL:
-                break
+                assert len(stack) == 1
+                return stacktop.expansion, token, offset
 
             if stacktop.parsestate == PARSESTATE_FUNCTION:
-                if c == ',':
+                if token == ',':
                     stacktop.function.append(stacktop.expansion)
                     stacktop.expansion = data.Expansion()
-                elif c == ')':
+                elif token == ')':
                     stacktop.function.append(stacktop.expansion)
                     stacktop.function.setup()
                     stack.pop()
                     stack[-1].expansion.append(stacktop.function)
                 else:
                     assert False, "Not reached, PARSESTATE_FUNCTION"
             elif stacktop.parsestate == PARSESTATE_VARNAME:
-                if c == ':':
+                if token == ':':
                     stacktop.varname = stacktop.expansion
                     stacktop.parsestate = PARSESTATE_SUBSTFROM
                     stacktop.expansion = data.Expansion()
-                    stacktop.stopon = '=)'
-                elif c == ')':
+                    stacktop.stopon = ('=', ')')
+                elif token == ')':
                     stack.pop()
                     stack[-1].expansion.append(functions.VariableRef(stacktop.loc, stacktop.expansion))
                 else:
                     assert False, "Not reached, PARSESTATE_VARNAME"
             elif stacktop.parsestate == PARSESTATE_SUBSTFROM:
-                if c == '=':
+                if token == '=':
                     stacktop.substfrom = stacktop.expansion
                     stacktop.parsestate = PARSESTATE_SUBSTTO
                     stacktop.expansion = data.Expansion()
-                    stacktop.stopon = ')'
-                elif c == ')':
+                    stacktop.stopon = (')',)
+                elif token == ')':
                     # A substitution of the form $(VARNAME:.ee) is probably a mistake, but make
                     # parses it. Issue a warning. Combine the varname and substfrom expansions to
                     # make the compatible varname. See tests/var-substitutions.mk SIMPLE3SUBSTNAME
                     log.warning("%s: Variable reference looks like substitution without =" % (stacktop.loc, ))
                     stacktop.varname.append(':')
                     stacktop.varname.concat(stacktop.expansion)
                     stack.pop()
                     stack[-1].expansion.append(functions.VariableRef(stacktop.loc, stacktop.varname))
                 else:
                     assert False, "Not reached, PARSESTATE_SUBSTFROM"
             elif stacktop.parsestate == PARSESTATE_SUBSTTO:
-                assert c == ')', "Not reached, PARSESTATE_SUBSTTO"
+                assert token == ')', "Not reached, PARSESTATE_SUBSTTO"
 
                 stack.pop()
                 stack[-1].expansion.append(functions.SubstitutionRef(stacktop.loc, stacktop.varname,
                                                                      stacktop.substfrom, stacktop.expansion))
             else:
                 assert False, "Unexpected parse state %s" % stacktop.parsestate
+
+            continue
+        elif c == '$':
+            try:
+                c, offset, loc = di.next()
+            except StopIteration:
+                # an un-terminated $ expands to nothing
+                break
+
+            if c == '$':
+                stacktop.expansion.append('$')
+                continue
+
+            if c == '(':
+                # look forward for a function name
+                fname, offset = d.findtoken(offset + 1, functiontokens, True)
+                if fname is not None:
+                    fn = functions.functionmap[fname](loc)
+                    stack.append(ParseStackFrame(PARSESTATE_FUNCTION,
+                                                 data.Expansion(), ',)',
+                                                 function=fn))
+                    di = iterfunc(d, offset)
+                    continue
+
+                e = data.Expansion()
+                stack.append(ParseStackFrame(PARSESTATE_VARNAME, e, (':', ')'), loc=loc))
+                continue
+
+            fe = data.Expansion()
+            fe.append(c)
+            stacktop.expansion.append(functions.VariableRef(loc, fe))
+            continue
+
         else:
             stacktop.expansion.append(c)
-        i += 1
 
     if len(stack) != 1:
         raise SyntaxError("Unterminated function call", d.getloc(len(d) - 1))
 
     assert stack[0].parsestate == PARSESTATE_TOPLEVEL
 
-    assert i <= len(d), 'overwrote the end: i=%i len(d)=%i' % (i, len(d))
-
-    if i == len(d):
-        i = -1
-    return stack[0].expansion, i
+    return stack[0].expansion, None, None
--- a/tests/parsertests.py
+++ b/tests/parsertests.py
@@ -1,20 +1,20 @@
 import pymake.data, pymake.parser, pymake.functions
 import unittest
 import logging
 
 from cStringIO import StringIO
 
 def multitest(cls):
-    for i in xrange(0, len(cls.testdata)):
-        def m(self, i=i):
-            return self.runSingle(*self.testdata[i])
+    for name in cls.testdata.iterkeys():
+        def m(self, name=name):
+            return self.runSingle(*self.testdata[name])
 
-        setattr(cls, 'test_%i' % i, m)
+        setattr(cls, 'test_%s' % name, m)
     return cls
 
 class TestBase(unittest.TestCase):
     def assertEqual(self, a, b, msg=""):
         """Actually print the values which weren't equal, if things don't work out!"""
         unittest.TestCase.assertEqual(self, a, b, "%s got %r expected %r" % (msg, a, b))
 
 class DataTest(TestBase):
@@ -31,135 +31,159 @@ class DataTest(TestBase):
             for line, file, lineno, col in datas:
                 d.append(line, pymake.parser.Location(file, lineno, col))
             for pos, file, lineno, col in results:
                 loc = d.getloc(pos)
                 self.assertEqual(loc.path, file, "data file")
                 self.assertEqual(loc.line, lineno, "data line")
                 self.assertEqual(loc.column, col, "data %r col, got %i expected %i" % (d.data, loc.column, col))
 
+class TokenTest(TestBase):
+    testdata = {
+        'nomatch': ('string data', 0, ('+=', ':=', '='), False, None, 0),
+        'match': ('string = val', 0, ('+=', ':=', '='), False, '=', 8),
+        'firstmatch': ('string += val', 0, ('+=', ':=', '='), False, '+=', 9),
+        'startpos': ('string += val = var', 10, ('+=', ':=', '='), False, '=', 14),
+        'wsmatch': ('  ifdef FOO', 2, ('ifdef', 'else'), True, 'ifdef', 8),
+        'wsnomatch': ('  unexpected FOO', 2, ('ifdef', 'else'), True, None, 2),
+        'wsnows': ('  ifdefFOO', 2, ('ifdef', 'else'), True, None, 2),
+        'paren': ('$(FOO)', 5, '=)', False, ')', 6),
+        }
+
+    def runSingle(self, s, start, tlist, needws, etoken, eoffset):
+        d = Data(None, None)
+        d.append(s, None)
+        atoken, aoffset = d.findtoken(start, tlist)
+        self.assertEqual(atoken, etoken)
+        self.assertEqual(aoffset, eoffset)
+
 class IterTest(TestBase):
-    testdata = (
-        (
+    testdata = {
+        'plaindata': (
             pymake.parser.iterdata,
             "plaindata # test\n",
             "plaindata # test\n"
-        ),
-        (
+            ),
+        'makecomment': (
             pymake.parser.itermakefilechars,
             "VAR = val # comment",
             "VAR = val "
-        ),
-        (
+            ),
+        'makeescapedcomment': (
             pymake.parser.itermakefilechars,
             "VAR = val \# escaped hash\n",
             "VAR = val # escaped hash"
-        ),
-        (
+            ),
+        'makeescapedslash': (
+            pymake.parser.itermakefilechars,
+            "VAR = val\\\\\n",
+            "VAR = val\\\\",
+            ),
+        'makecontinuation': (
             pymake.parser.itermakefilechars,
             "VAR = VAL  \\\n  continuation # comment \\\n  continuation",
             "VAR = VAL continuation "
-        ),
-        (
+            ),
+        'makeawful': (
             pymake.parser.itermakefilechars,
             "VAR = VAL  \\\\# comment\n",
             "VAR = VAL  \\"
-        ),
-        (
+            ),
+        'command': (
             pymake.parser.itercommandchars,
             "echo boo # comment\n",
             "echo boo # comment",
-        ),
-        (
+            ),
+        'commandcomment': (
             pymake.parser.itercommandchars,
             "echo boo \# comment\n",
             "echo boo \# comment",
-        ),
-        (
+            ),
+        'commandcontinue': (
             pymake.parser.itercommandchars,
             "echo boo # \\\n\t  command 2\n",
             "echo boo # \\\n  command 2"
-        ),
-        (
+            ),
+        'define': (
             pymake.parser.iterdefinechars,
             "endef",
             ""
-        ),
-        (
+            ),
+        'definenesting': (
             pymake.parser.iterdefinechars,
             """define BAR # comment
 random text
 endef not what you think!
 endef # comment is ok\n""",
             """define BAR # comment
 random text
 endef not what you think!"""
-        ),
-        (
+            ),
+        'defineescaped': (
             pymake.parser.iterdefinechars,
-            """value \\
+            """value   \\
 endef
 endef\n""",
-            "value \\\nendef"
+            "value endef"
         ),
-    )
+    }
 
     def runSingle(self, ifunc, idata, expected):
         fd = StringIO(idata)
         lineiter = pymake.parser.iterlines(fd)
 
         d = pymake.parser.Data(lineiter, 'PlainIterTest-data')
         d.readline()
 
         actual = ''.join( (c for c, offset, location in ifunc(d, 0)) )
         self.assertEqual(actual, expected)
 
         self.assertRaises(StopIteration, lambda: fd.next())
 multitest(IterTest)
 
 class MakeSyntaxTest(TestBase):
     # (string, startat, stopat, stopoffset, expansion
-    testdata = (
-        ('hello world', 0, '', -1, ['hello world']),
-        ('hello $W', 0, '', -1,
-         ['hello ',
-          {'type': 'VariableRef',
-           '.vname': ['W']}
-          ]),
-        ('hello: world', 0, ':=', 5, ['hello']),
-        ('h $(flavor FOO)', 0, '', -1,
-         ['h ',
-          {'type': 'FlavorFunction',
-           '[0]': ['FOO']}
-          ]),
-        ('hello$$world', 0, '', -1, ['hello$world']),
-        ('echo $(VAR)', 0, '', -1,
-         ['echo ',
-          {'type': 'VariableRef',
-           '.vname': ['VAR']}
-          ]),
-        ('echo $($(VARNAME):.c=.o)', 0, '', -1,
-         ['echo ',
-          {'type': 'SubstitutionRef',
-           '.vname': [{'type': 'VariableRef',
-                       '.vname': ['VARNAME']}
-                      ],
-           '.substfrom': ['.c'],
-           '.substto': ['.o']}
-          ]),
-        ('  $(VAR:VAL) = $(VAL)', 0, ':=', 13,
-         ['  ',
-          {'type': 'VariableRef',
-           '.vname': ['VAR:VAL']},
-          ' ']),
-        ('  $(VAR:VAL) = $(VAL)', 15, '', -1,
-         [{'type': 'VariableRef',
-           '.vname': ['VAL']},
-         ]),
-    )
+    testdata = {
+        'text': ('hello world', 0, (), None, ['hello world']),
+        'singlechar': ('hello $W', 0, (), None,
+                       ['hello ',
+                        {'type': 'VariableRef',
+                         '.vname': ['W']}
+                        ]),
+        'stopat': ('hello: world', 0, (':', '='), 6, ['hello']),
+        'funccall': ('h $(flavor FOO)', 0, (), None,
+                     ['h ',
+                      {'type': 'FlavorFunction',
+                       '[0]': ['FOO']}
+                      ]),
+        'escapedollar': ('hello$$world', 0, (), None, ['hello$world']),
+        'varref': ('echo $(VAR)', 0, (), None,
+                   ['echo ',
+                    {'type': 'VariableRef',
+                     '.vname': ['VAR']}
+                    ]),
+        'dynamicvarname': ('echo $($(VARNAME):.c=.o)', 0, (':'), None,
+                           ['echo ',
+                            {'type': 'SubstitutionRef',
+                             '.vname': [{'type': 'VariableRef',
+                                         '.vname': ['VARNAME']}
+                                        ],
+                             '.substfrom': ['.c'],
+                             '.substto': ['.o']}
+                            ]),
+        'substref': ('  $(VAR:VAL) := $(VAL)', 0, (':=', '+=', '=', ':'), 15,
+                     ['  ',
+                      {'type': 'VariableRef',
+                       '.vname': ['VAR:VAL']},
+                      ' ']),
+        'vadsubstref': ('  $(VAR:VAL) = $(VAL)', 15, (), None,
+                        [{'type': 'VariableRef',
+                          '.vname': ['VAL']},
+                         ]),
+        }
 
     def compareRecursive(self, actual, expected, path):
         self.assertEqual(len(actual), len(expected),
                          "compareRecursive: %s" % (path,))
         for i in xrange(0, len(actual)):
             ipath = path + [i]
 
             a = actual[i]
@@ -178,24 +202,25 @@ class MakeSyntaxTest(TestBase):
                         self.compareRecursive(a[item], v, proppath)
                     elif k[0] == '.':
                         item = k[1:]
                         proppath = ipath + [item]
                         self.compareRecursive(getattr(a, item), v, proppath)
                     else:
                         raise Exception("Unexpected property at %s: %s" % (ipath, k))
 
-    def runTest(self):
-        for s, startat, stopat, stopoffset, expansion in self.testdata:
-            d = pymake.parser.Data(None, None)
-            d.append(s, pymake.parser.Location('testdata', 1, 0))
+    def runSingle(self, s, startat, stopat, stopoffset, expansion):
+        d = pymake.parser.Data(None, None)
+        d.append(s, pymake.parser.Location('testdata', 1, 0))
 
-            a, stoppedat = pymake.parser.parsemakesyntax(d, startat, stopat, pymake.parser.PARSESTYLE_MAKEFILE)
-            self.compareRecursive(a, expansion, [])
-            self.assertEqual(stoppedat, stopoffset)
+        a, t, offset = pymake.parser.parsemakesyntax(d, startat, stopat, pymake.parser.itermakefilechars)
+        self.compareRecursive(a, expansion, [])
+        self.assertEqual(offset, stopoffset)
+
+multitest(MakeSyntaxTest)
 
 class VariableTest(TestBase):
     testdata = """
     VAR = value
     VARNAME = TESTVAR
     $(VARNAME) = testvalue
     $(VARNAME:VAR=VAL) = moretesting
     IMM := $(VARNAME) # this is a comment
new file mode 100644
--- /dev/null
+++ b/tests/unterminated-dollar.mk
@@ -0,0 +1,6 @@
+VAR = value$
+VAR2 = other
+
+all:
+	test "$(VAR)" = "value"
+	@echo TEST-PASS