blob: 4bff5336881718419dcc427c6460fdd4f0f52792 [file] [log] [blame]
"""
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