summaryrefslogtreecommitdiffstats
path: root/build/pymake/pymake/parser.py
diff options
context:
space:
mode:
Diffstat (limited to 'build/pymake/pymake/parser.py')
-rw-r--r--build/pymake/pymake/parser.py822
1 files changed, 822 insertions, 0 deletions
diff --git a/build/pymake/pymake/parser.py b/build/pymake/pymake/parser.py
new file mode 100644
index 000000000..4bff53368
--- /dev/null
+++ b/build/pymake/pymake/parser.py
@@ -0,0 +1,822 @@
+"""
+Module for parsing Makefile syntax.
+
+Makefiles use a line-based parsing system. Continuations and substitutions are handled differently based on the
+type of line being parsed:
+
+Lines with makefile syntax condense continuations to a single space, no matter the actual trailing whitespace
+of the first line or the leading whitespace of the continuation. In other situations, trailing whitespace is
+relevant.
+
+Lines with command syntax do not condense continuations: the backslash and newline are part of the command.
+(GNU Make is buggy in this regard, at least on mac).
+
+Lines with an initial tab are commands if they can be (there is a rule or a command immediately preceding).
+Otherwise, they are parsed as makefile syntax.
+
+This file parses into the data structures defined in the parserdata module. Those classes are what actually
+do the dirty work of "executing" the parsed data into a data.Makefile.
+
+Four iterator functions are available:
+* iterdata
+* itermakefilechars
+* itercommandchars
+
+The iterators handle line continuations and comments in different ways, but share a common calling
+convention:
+
+Called with (data, startoffset, tokenlist, finditer)
+
+yield 4-tuples (flatstr, token, tokenoffset, afteroffset)
+flatstr is data, guaranteed to have no tokens (may be '')
+token, tokenoffset, afteroffset *may be None*. That means there is more text
+coming.
+"""
+
+import logging, re, os, sys
+import data, functions, util, parserdata
+
+_log = logging.getLogger('pymake.parser')
+
+class SyntaxError(util.MakeError):
+ pass
+
+_skipws = re.compile('\S')
+class Data(object):
+ """
+ A single virtual "line", which can be multiple source lines joined with
+ continuations.
+ """
+
+ __slots__ = ('s', 'lstart', 'lend', 'loc')
+
+ def __init__(self, s, lstart, lend, loc):
+ self.s = s
+ self.lstart = lstart
+ self.lend = lend
+ self.loc = loc
+
+ @staticmethod
+ def fromstring(s, path):
+ return Data(s, 0, len(s), parserdata.Location(path, 1, 0))
+
+ def getloc(self, offset):
+ assert offset >= self.lstart and offset <= self.lend
+ return self.loc.offset(self.s, self.lstart, offset)
+
+ def skipwhitespace(self, offset):
+ """
+ Return the offset of the first non-whitespace character in data starting at offset, or None if there are
+ only whitespace characters remaining.
+ """
+ m = _skipws.search(self.s, offset, self.lend)
+ if m is None:
+ return self.lend
+
+ return m.start(0)
+
+_linere = re.compile(r'\\*\n')
+def enumeratelines(s, filename):
+ """
+ Enumerate lines in a string as Data objects, joining line
+ continuations.
+ """
+
+ off = 0
+ lineno = 1
+ curlines = 0
+ for m in _linere.finditer(s):
+ curlines += 1
+ start, end = m.span(0)
+
+ if (start - end) % 2 == 0:
+ # odd number of backslashes is a continuation
+ continue
+
+ yield Data(s, off, end - 1, parserdata.Location(filename, lineno, 0))
+
+ lineno += curlines
+ curlines = 0
+ off = end
+
+ yield Data(s, off, len(s), parserdata.Location(filename, lineno, 0))
+
+_alltokens = re.compile(r'''\\*\# | # hash mark preceeded by any number of backslashes
+ := |
+ \+= |
+ \?= |
+ :: |
+ (?:\$(?:$|[\(\{](?:%s)\s+|.)) | # dollar sign followed by EOF, a function keyword with whitespace, or any character
+ :(?![\\/]) | # colon followed by anything except a slash (Windows path detection)
+ [=#{}();,|'"]''' % '|'.join(functions.functionmap.iterkeys()), re.VERBOSE)
+
+def iterdata(d, offset, tokenlist, it):
+ """
+ Iterate over flat data without line continuations, comments, or any special escaped characters.
+
+ Typically used to parse recursively-expanded variables.
+ """
+
+ assert len(tokenlist), "Empty tokenlist passed to iterdata is meaningless!"
+ assert offset >= d.lstart and offset <= d.lend, "offset %i should be between %i and %i" % (offset, d.lstart, d.lend)
+
+ if offset == d.lend:
+ return
+
+ s = d.s
+ for m in it:
+ mstart, mend = m.span(0)
+ token = s[mstart:mend]
+ if token in tokenlist or (token[0] == '$' and '$' in tokenlist):
+ yield s[offset:mstart], token, mstart, mend
+ else:
+ yield s[offset:mend], None, None, mend
+ offset = mend
+
+ yield s[offset:d.lend], None, None, None
+
+# multiple backslashes before a newline are unescaped, halving their total number
+_makecontinuations = re.compile(r'(?:\s*|((?:\\\\)+))\\\n\s*')
+def _replacemakecontinuations(m):
+ start, end = m.span(1)
+ if start == -1:
+ return ' '
+ return ' '.rjust((end - start) / 2 + 1, '\\')
+
+def itermakefilechars(d, offset, tokenlist, it, ignorecomments=False):
+ """
+ Iterate over data in makefile syntax. Comments are found at unescaped # characters, and escaped newlines
+ are converted to single-space continuations.
+ """
+
+ assert offset >= d.lstart and offset <= d.lend, "offset %i should be between %i and %i" % (offset, d.lstart, d.lend)
+
+ if offset == d.lend:
+ return
+
+ s = d.s
+ for m in it:
+ mstart, mend = m.span(0)
+ token = s[mstart:mend]
+
+ starttext = _makecontinuations.sub(_replacemakecontinuations, s[offset:mstart])
+
+ if token[-1] == '#' and not ignorecomments:
+ l = mend - mstart
+ # multiple backslashes before a hash are unescaped, halving their total number
+ if l % 2:
+ # found a comment
+ yield starttext + token[:(l - 1) / 2], None, None, None
+ return
+ else:
+ yield starttext + token[-l / 2:], None, None, mend
+ elif token in tokenlist or (token[0] == '$' and '$' in tokenlist):
+ yield starttext, token, mstart, mend
+ else:
+ yield starttext + token, None, None, mend
+ offset = mend
+
+ yield _makecontinuations.sub(_replacemakecontinuations, s[offset:d.lend]), None, None, None
+
+_findcomment = re.compile(r'\\*\#')
+def flattenmakesyntax(d, offset):
+ """
+ A shortcut method for flattening line continuations and comments in makefile syntax without
+ looking for other tokens.
+ """
+
+ assert offset >= d.lstart and offset <= d.lend, "offset %i should be between %i and %i" % (offset, d.lstart, d.lend)
+ if offset == d.lend:
+ return ''
+
+ s = _makecontinuations.sub(_replacemakecontinuations, d.s[offset:d.lend])
+
+ elements = []
+ offset = 0
+ for m in _findcomment.finditer(s):
+ mstart, mend = m.span(0)
+ elements.append(s[offset:mstart])
+ if (mend - mstart) % 2:
+ # even number of backslashes... it's a comment
+ elements.append(''.ljust((mend - mstart - 1) / 2, '\\'))
+ return ''.join(elements)
+
+ # odd number of backslashes
+ elements.append(''.ljust((mend - mstart - 2) / 2, '\\') + '#')
+ offset = mend
+
+ elements.append(s[offset:])
+ return ''.join(elements)
+
+def itercommandchars(d, offset, tokenlist, it):
+ """
+ Iterate over command syntax. # comment markers are not special, and escaped newlines are included
+ in the output text.
+ """
+
+ assert offset >= d.lstart and offset <= d.lend, "offset %i should be between %i and %i" % (offset, d.lstart, d.lend)
+
+ if offset == d.lend:
+ return
+
+ s = d.s
+ for m in it:
+ mstart, mend = m.span(0)
+ token = s[mstart:mend]
+ starttext = s[offset:mstart].replace('\n\t', '\n')
+
+ if token in tokenlist or (token[0] == '$' and '$' in tokenlist):
+ yield starttext, token, mstart, mend
+ else:
+ yield starttext + token, None, None, mend
+ offset = mend
+
+ yield s[offset:d.lend].replace('\n\t', '\n'), None, None, None
+
+_redefines = re.compile('\s*define|\s*endef')
+def iterdefinelines(it, startloc):
+ """
+ Process the insides of a define. Most characters are included literally. Escaped newlines are treated
+ as they would be in makefile syntax. Internal define/endef pairs are ignored.
+ """
+
+ results = []
+
+ definecount = 1
+ for d in it:
+ m = _redefines.match(d.s, d.lstart, d.lend)
+ if m is not None:
+ directive = m.group(0).strip()
+ if directive == 'endef':
+ definecount -= 1
+ if definecount == 0:
+ return _makecontinuations.sub(_replacemakecontinuations, '\n'.join(results))
+ else:
+ definecount += 1
+
+ results.append(d.s[d.lstart:d.lend])
+
+ # Falling off the end is an unterminated define!
+ raise SyntaxError("define without matching endef", startloc)
+
+def _ensureend(d, offset, msg):
+ """
+ Ensure that only whitespace remains in this data.
+ """
+
+ s = flattenmakesyntax(d, offset)
+ if s != '' and not s.isspace():
+ raise SyntaxError(msg, d.getloc(offset))
+
+_eqargstokenlist = ('(', "'", '"')
+
+def ifeq(d, offset):
+ if offset > d.lend - 1:
+ raise SyntaxError("No arguments after conditional", d.getloc(offset))
+
+ # the variety of formats for this directive is rather maddening
+ token = d.s[offset]
+ if token not in _eqargstokenlist:
+ raise SyntaxError("No arguments after conditional", d.getloc(offset))
+
+ offset += 1
+
+ if token == '(':
+ arg1, t, offset = parsemakesyntax(d, offset, (',',), itermakefilechars)
+ if t is None:
+ raise SyntaxError("Expected two arguments in conditional", d.getloc(d.lend))
+
+ arg1.rstrip()
+
+ offset = d.skipwhitespace(offset)
+ arg2, t, offset = parsemakesyntax(d, offset, (')',), itermakefilechars)
+ if t is None:
+ raise SyntaxError("Unexpected text in conditional", 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(d.lend))
+
+ offset = d.skipwhitespace(offset)
+ if offset == d.lend:
+ raise SyntaxError("Expected two arguments in conditional", d.getloc(offset))
+
+ token = d.s[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")
+
+ return parserdata.EqCondition(arg1, arg2)
+
+def ifneq(d, offset):
+ c = ifeq(d, offset)
+ c.expected = False
+ return c
+
+def ifdef(d, offset):
+ e, t, offset = parsemakesyntax(d, offset, (), itermakefilechars)
+ e.rstrip()
+
+ return parserdata.IfdefCondition(e)
+
+def ifndef(d, offset):
+ c = ifdef(d, offset)
+ c.expected = False
+ return c
+
+_conditionkeywords = {
+ 'ifeq': ifeq,
+ 'ifneq': ifneq,
+ 'ifdef': ifdef,
+ 'ifndef': ifndef
+ }
+
+_conditiontokens = tuple(_conditionkeywords.iterkeys())
+_conditionre = re.compile(r'(%s)(?:$|\s+)' % '|'.join(_conditiontokens))
+
+_directivestokenlist = _conditiontokens + \
+ ('else', 'endif', 'define', 'endef', 'override', 'include', '-include', 'includedeps', '-includedeps', 'vpath', 'export', 'unexport')
+
+_directivesre = re.compile(r'(%s)(?:$|\s+)' % '|'.join(_directivestokenlist))
+
+_varsettokens = (':=', '+=', '?=', '=')
+
+def _parsefile(pathname):
+ fd = open(pathname, "rU")
+ stmts = parsestring(fd.read(), pathname)
+ stmts.mtime = os.fstat(fd.fileno()).st_mtime
+ fd.close()
+ return stmts
+
+def _checktime(path, stmts):
+ mtime = os.path.getmtime(path)
+ if mtime != stmts.mtime:
+ _log.debug("Re-parsing makefile '%s': mtimes differ", path)
+ return False
+
+ return True
+
+_parsecache = util.MostUsedCache(50, _parsefile, _checktime)
+
+def parsefile(pathname):
+ """
+ Parse a filename into a parserdata.StatementList. A cache is used to avoid re-parsing
+ makefiles that have already been parsed and have not changed.
+ """
+
+ pathname = os.path.realpath(pathname)
+ return _parsecache.get(pathname)
+
+# colon followed by anything except a slash (Windows path detection)
+_depfilesplitter = re.compile(r':(?![\\/])')
+# simple variable references
+_vars = re.compile('\$\((\w+)\)')
+
+def parsedepfile(pathname):
+ """
+ Parse a filename listing only depencencies into a parserdata.StatementList.
+ Simple variable references are allowed in such files.
+ """
+ def continuation_iter(lines):
+ current_line = []
+ for line in lines:
+ line = line.rstrip()
+ if line.endswith("\\"):
+ current_line.append(line.rstrip("\\"))
+ continue
+ if not len(line):
+ continue
+ current_line.append(line)
+ yield ''.join(current_line)
+ current_line = []
+ if current_line:
+ yield ''.join(current_line)
+
+ def get_expansion(s):
+ if '$' in s:
+ expansion = data.Expansion()
+ # for an input like e.g. "foo $(bar) baz",
+ # _vars.split returns ["foo", "bar", "baz"]
+ # every other element is a variable name.
+ for i, element in enumerate(_vars.split(s)):
+ if i % 2:
+ expansion.appendfunc(functions.VariableRef(None,
+ data.StringExpansion(element, None)))
+ elif element:
+ expansion.appendstr(element)
+
+ return expansion
+
+ return data.StringExpansion(s, None)
+
+ pathname = os.path.realpath(pathname)
+ stmts = parserdata.StatementList()
+ for line in continuation_iter(open(pathname).readlines()):
+ target, deps = _depfilesplitter.split(line, 1)
+ stmts.append(parserdata.Rule(get_expansion(target),
+ get_expansion(deps), False))
+ return stmts
+
+def parsestring(s, filename):
+ """
+ Parse a string containing makefile data into a parserdata.StatementList.
+ """
+
+ currule = False
+ condstack = [parserdata.StatementList()]
+
+ fdlines = enumeratelines(s, filename)
+ for d in fdlines:
+ assert len(condstack) > 0
+
+ offset = d.lstart
+
+ if currule and offset < d.lend and d.s[offset] == '\t':
+ e, token, offset = parsemakesyntax(d, offset + 1, (), itercommandchars)
+ assert token is None
+ assert offset is None
+ condstack[-1].append(parserdata.Command(e))
+ continue
+
+ # 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.
+
+ offset = d.skipwhitespace(offset)
+ if offset is None:
+ continue
+
+ m = _directivesre.match(d.s, offset, d.lend)
+ if m is not None:
+ kword = m.group(1)
+ offset = m.end(0)
+
+ if kword == 'endif':
+ _ensureend(d, offset, "Unexpected data after 'endif' directive")
+ if len(condstack) == 1:
+ raise SyntaxError("unmatched 'endif' directive",
+ d.getloc(offset))
+
+ condstack.pop().endloc = d.getloc(offset)
+ continue
+
+ if kword == 'else':
+ if len(condstack) == 1:
+ raise SyntaxError("unmatched 'else' directive",
+ d.getloc(offset))
+
+ m = _conditionre.match(d.s, offset, d.lend)
+ if m is None:
+ _ensureend(d, offset, "Unexpected data after 'else' directive.")
+ condstack[-1].addcondition(d.getloc(offset), parserdata.ElseCondition())
+ else:
+ kword = m.group(1)
+ if kword not in _conditionkeywords:
+ raise SyntaxError("Unexpected condition after 'else' directive.",
+ d.getloc(offset))
+
+ startoffset = offset
+ offset = d.skipwhitespace(m.end(1))
+ c = _conditionkeywords[kword](d, offset)
+ condstack[-1].addcondition(d.getloc(startoffset), c)
+ continue
+
+ if kword in _conditionkeywords:
+ c = _conditionkeywords[kword](d, offset)
+ cb = parserdata.ConditionBlock(d.getloc(d.lstart), c)
+ condstack[-1].append(cb)
+ condstack.append(cb)
+ continue
+
+ if kword == 'endef':
+ raise SyntaxError("endef without matching define", d.getloc(offset))
+
+ if kword == 'define':
+ currule = False
+ vname, t, i = parsemakesyntax(d, offset, (), itermakefilechars)
+ vname.rstrip()
+
+ startloc = d.getloc(d.lstart)
+ value = iterdefinelines(fdlines, startloc)
+ condstack[-1].append(parserdata.SetVariable(vname, value=value, valueloc=startloc, token='=', targetexp=None))
+ continue
+
+ if kword in ('include', '-include', 'includedeps', '-includedeps'):
+ if kword.startswith('-'):
+ required = False
+ kword = kword[1:]
+ else:
+ required = True
+
+ deps = kword == 'includedeps'
+
+ currule = False
+ incfile, t, offset = parsemakesyntax(d, offset, (), itermakefilechars)
+ condstack[-1].append(parserdata.Include(incfile, required, deps))
+
+ continue
+
+ if kword == 'vpath':
+ currule = False
+ e, t, offset = parsemakesyntax(d, offset, (), itermakefilechars)
+ condstack[-1].append(parserdata.VPathDirective(e))
+ continue
+
+ if kword == 'override':
+ currule = False
+ vname, token, offset = parsemakesyntax(d, offset, _varsettokens, itermakefilechars)
+ vname.lstrip()
+ vname.rstrip()
+
+ if token is None:
+ raise SyntaxError("Malformed override directive, need =", d.getloc(d.lstart))
+
+ value = flattenmakesyntax(d, offset).lstrip()
+
+ condstack[-1].append(parserdata.SetVariable(vname, value=value, valueloc=d.getloc(offset), token=token, targetexp=None, source=data.Variables.SOURCE_OVERRIDE))
+ continue
+
+ if kword == 'export':
+ currule = False
+ e, token, offset = parsemakesyntax(d, offset, _varsettokens, itermakefilechars)
+ e.lstrip()
+ e.rstrip()
+
+ if token is None:
+ condstack[-1].append(parserdata.ExportDirective(e, concurrent_set=False))
+ else:
+ condstack[-1].append(parserdata.ExportDirective(e, concurrent_set=True))
+
+ value = flattenmakesyntax(d, offset).lstrip()
+ condstack[-1].append(parserdata.SetVariable(e, value=value, valueloc=d.getloc(offset), token=token, targetexp=None))
+
+ continue
+
+ if kword == 'unexport':
+ e, token, offset = parsemakesyntax(d, offset, (), itermakefilechars)
+ condstack[-1].append(parserdata.UnexportDirective(e))
+ continue
+
+ e, token, offset = parsemakesyntax(d, offset, _varsettokens + ('::', ':'), itermakefilechars)
+ if token is None:
+ e.rstrip()
+ e.lstrip()
+ if not e.isempty():
+ condstack[-1].append(parserdata.EmptyDirective(e))
+ continue
+
+ # if we encountered real makefile syntax, the current rule is over
+ currule = False
+
+ if token in _varsettokens:
+ e.lstrip()
+ e.rstrip()
+
+ value = flattenmakesyntax(d, offset).lstrip()
+
+ condstack[-1].append(parserdata.SetVariable(e, value=value, valueloc=d.getloc(offset), token=token, targetexp=None))
+ else:
+ 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 = e
+
+ e, token, offset = parsemakesyntax(d, offset,
+ _varsettokens + (':', '|', ';'),
+ itermakefilechars)
+ if token in (None, ';'):
+ condstack[-1].append(parserdata.Rule(targets, e, doublecolon))
+ currule = True
+
+ if token == ';':
+ offset = d.skipwhitespace(offset)
+ e, t, offset = parsemakesyntax(d, offset, (), itercommandchars)
+ condstack[-1].append(parserdata.Command(e))
+
+ elif token in _varsettokens:
+ e.lstrip()
+ e.rstrip()
+
+ value = flattenmakesyntax(d, offset).lstrip()
+ condstack[-1].append(parserdata.SetVariable(e, value=value, valueloc=d.getloc(offset), token=token, targetexp=targets))
+ elif token == '|':
+ raise SyntaxError('order-only prerequisites not implemented', d.getloc(offset))
+ else:
+ assert token == ':'
+ # static pattern rule
+
+ pattern = e
+
+ deps, token, offset = parsemakesyntax(d, offset, (';',), itermakefilechars)
+
+ condstack[-1].append(parserdata.StaticPatternRule(targets, pattern, deps, doublecolon))
+ currule = True
+
+ if token == ';':
+ offset = d.skipwhitespace(offset)
+ e, token, offset = parsemakesyntax(d, offset, (), itercommandchars)
+ condstack[-1].append(parserdata.Command(e))
+
+ if len(condstack) != 1:
+ raise SyntaxError("Condition never terminated with endif", condstack[-1].loc)
+
+ return condstack[0]
+
+_PARSESTATE_TOPLEVEL = 0 # at the top level
+_PARSESTATE_FUNCTION = 1 # expanding a function call
+_PARSESTATE_VARNAME = 2 # expanding a variable expansion.
+_PARSESTATE_SUBSTFROM = 3 # expanding a variable expansion substitution "from" value
+_PARSESTATE_SUBSTTO = 4 # expanding a variable expansion substitution "to" value
+_PARSESTATE_PARENMATCH = 5 # inside nested parentheses/braces that must be matched
+
+class ParseStackFrame(object):
+ __slots__ = ('parsestate', 'parent', 'expansion', 'tokenlist', 'openbrace', 'closebrace', 'function', 'loc', 'varname', 'substfrom')
+
+ def __init__(self, parsestate, parent, expansion, tokenlist, openbrace, closebrace, function=None, loc=None):
+ self.parsestate = parsestate
+ self.parent = parent
+ self.expansion = expansion
+ self.tokenlist = tokenlist
+ self.openbrace = openbrace
+ self.closebrace = closebrace
+ self.function = function
+ self.loc = loc
+
+ def __str__(self):
+ return "<state=%i expansion=%s tokenlist=%s openbrace=%s closebrace=%s>" % (self.parsestate, self.expansion, self.tokenlist, self.openbrace, self.closebrace)
+
+_matchingbrace = {
+ '(': ')',
+ '{': '}',
+ }
+
+def parsemakesyntax(d, offset, 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, token, offset). If all the data is consumed,
+ token and offset will be None
+ """
+
+ assert callable(iterfunc)
+
+ stacktop = ParseStackFrame(_PARSESTATE_TOPLEVEL, None, data.Expansion(loc=d.getloc(d.lstart)),
+ tokenlist=stopon + ('$',),
+ openbrace=None, closebrace=None)
+
+ tokeniterator = _alltokens.finditer(d.s, offset, d.lend)
+
+ di = iterfunc(d, offset, stacktop.tokenlist, tokeniterator)
+ while True: # this is not a for loop because `di` changes during the function
+ assert stacktop is not None
+ try:
+ s, token, tokenoffset, offset = di.next()
+ except StopIteration:
+ break
+
+ stacktop.expansion.appendstr(s)
+ if token is None:
+ continue
+
+ parsestate = stacktop.parsestate
+
+ if token[0] == '$':
+ if tokenoffset + 1 == d.lend:
+ # an unterminated $ expands to nothing
+ break
+
+ loc = d.getloc(tokenoffset)
+ c = token[1]
+ if c == '$':
+ assert len(token) == 2
+ stacktop.expansion.appendstr('$')
+ elif c in ('(', '{'):
+ closebrace = _matchingbrace[c]
+
+ if len(token) > 2:
+ fname = token[2:].rstrip()
+ fn = functions.functionmap[fname](loc)
+ e = data.Expansion()
+ if len(fn) + 1 == fn.maxargs:
+ tokenlist = (c, closebrace, '$')
+ else:
+ tokenlist = (',', c, closebrace, '$')
+
+ stacktop = ParseStackFrame(_PARSESTATE_FUNCTION, stacktop,
+ e, tokenlist, function=fn,
+ openbrace=c, closebrace=closebrace)
+ else:
+ e = data.Expansion()
+ tokenlist = (':', c, closebrace, '$')
+ stacktop = ParseStackFrame(_PARSESTATE_VARNAME, stacktop,
+ e, tokenlist,
+ openbrace=c, closebrace=closebrace, loc=loc)
+ else:
+ assert len(token) == 2
+ e = data.Expansion.fromstring(c, loc)
+ stacktop.expansion.appendfunc(functions.VariableRef(loc, e))
+ elif token in ('(', '{'):
+ assert token == stacktop.openbrace
+
+ stacktop.expansion.appendstr(token)
+ stacktop = ParseStackFrame(_PARSESTATE_PARENMATCH, stacktop,
+ stacktop.expansion,
+ (token, stacktop.closebrace, '$'),
+ openbrace=token, closebrace=stacktop.closebrace, loc=d.getloc(tokenoffset))
+ elif parsestate == _PARSESTATE_PARENMATCH:
+ assert token == stacktop.closebrace
+ stacktop.expansion.appendstr(token)
+ stacktop = stacktop.parent
+ elif parsestate == _PARSESTATE_TOPLEVEL:
+ assert stacktop.parent is None
+ return stacktop.expansion.finish(), token, offset
+ elif parsestate == _PARSESTATE_FUNCTION:
+ if token == ',':
+ stacktop.function.append(stacktop.expansion.finish())
+
+ stacktop.expansion = data.Expansion()
+ if len(stacktop.function) + 1 == stacktop.function.maxargs:
+ tokenlist = (stacktop.openbrace, stacktop.closebrace, '$')
+ stacktop.tokenlist = tokenlist
+ elif token in (')', '}'):
+ fn = stacktop.function
+ fn.append(stacktop.expansion.finish())
+ fn.setup()
+
+ stacktop = stacktop.parent
+ stacktop.expansion.appendfunc(fn)
+ else:
+ assert False, "Not reached, _PARSESTATE_FUNCTION"
+ elif parsestate == _PARSESTATE_VARNAME:
+ if token == ':':
+ stacktop.varname = stacktop.expansion
+ stacktop.parsestate = _PARSESTATE_SUBSTFROM
+ stacktop.expansion = data.Expansion()
+ stacktop.tokenlist = ('=', stacktop.openbrace, stacktop.closebrace, '$')
+ elif token in (')', '}'):
+ fn = functions.VariableRef(stacktop.loc, stacktop.expansion.finish())
+ stacktop = stacktop.parent
+ stacktop.expansion.appendfunc(fn)
+ else:
+ assert False, "Not reached, _PARSESTATE_VARNAME"
+ elif parsestate == _PARSESTATE_SUBSTFROM:
+ if token == '=':
+ stacktop.substfrom = stacktop.expansion
+ stacktop.parsestate = _PARSESTATE_SUBSTTO
+ stacktop.expansion = data.Expansion()
+ stacktop.tokenlist = (stacktop.openbrace, stacktop.closebrace, '$')
+ elif token in (')', '}'):
+ # 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.appendstr(':')
+ stacktop.varname.concat(stacktop.expansion)
+ fn = functions.VariableRef(stacktop.loc, stacktop.varname.finish())
+ stacktop = stacktop.parent
+ stacktop.expansion.appendfunc(fn)
+ else:
+ assert False, "Not reached, _PARSESTATE_SUBSTFROM"
+ elif parsestate == _PARSESTATE_SUBSTTO:
+ assert token in (')','}'), "Not reached, _PARSESTATE_SUBSTTO"
+
+ fn = functions.SubstitutionRef(stacktop.loc, stacktop.varname.finish(),
+ stacktop.substfrom.finish(), stacktop.expansion.finish())
+ stacktop = stacktop.parent
+ stacktop.expansion.appendfunc(fn)
+ else:
+ assert False, "Unexpected parse state %s" % stacktop.parsestate
+
+ if stacktop.parent is not None and iterfunc == itercommandchars:
+ di = itermakefilechars(d, offset, stacktop.tokenlist, tokeniterator,
+ ignorecomments=True)
+ else:
+ di = iterfunc(d, offset, stacktop.tokenlist, tokeniterator)
+
+ if stacktop.parent is not None:
+ raise SyntaxError("Unterminated function call", d.getloc(offset))
+
+ assert stacktop.parsestate == _PARSESTATE_TOPLEVEL
+
+ return stacktop.expansion.finish(), None, None