text: 
  """Translator turning Scenic programs into Scenario objects.
  
  The top-level interface to Scenic is provided by two functions:
  
  * `scenarioFromString` -- compile a string of Scenic code;
  * `scenarioFromFile` -- compile a Scenic file.
  
  These output a `Scenario` object, from which scenes can be generated.
  See the documentation for `Scenario` for details.
  
  When imported, this module hooks the Python import system so that Scenic
  modules can be imported using the ``import`` statement. This is primarily for the
  translator's own use, but you could import Scenic modules from Python to
  inspect them. Because Scenic uses Python's import system, the latter's rules
  for finding modules apply, including the handling of packages.
  
  Scenic is compiled in two main steps: translating the code into Python, and
  executing the resulting Python module to generate a Scenario object encoding
  the objects, distributions, etc. in the scenario. For details, see the function
  `compileStream` below.
  """
  
  import sys
  import os
  import io
  import builtins
  import time
  import inspect
  import types
  import typing
  import importlib
  import importlib.abc
  import importlib.util
  import itertools
  import pathlib
  from collections import namedtuple, defaultdict
  from contextlib import contextmanager
  
  import tokenize
  from tokenize import NAME, NL, NEWLINE, ENDMARKER, OP, NUMBER, COLON, COMMENT, ENCODING
  from tokenize import LPAR, RPAR, LSQB, RSQB, RBRACE, COMMA, DOUBLESLASH, DOUBLESLASHEQUAL
  from tokenize import AT, LEFTSHIFT, RIGHTSHIFT, VBAR, AMPER, TILDE, CIRCUMFLEX, STAR
  from tokenize import LEFTSHIFTEQUAL, RIGHTSHIFTEQUAL, VBAREQUAL, AMPEREQUAL, CIRCUMFLEXEQUAL
  from tokenize import INDENT, DEDENT, STRING, SEMI
  
  import ast
  from ast import parse, dump, NodeVisitor, NodeTransformer, copy_location, fix_missing_locations
  from ast import Load, Store, Del, Name, Call, Tuple, BinOp, MatMult, BitAnd, BitOr, BitXor, LShift
  from ast import RShift, Starred, Lambda, AnnAssign, Set, Str, Subscript, Index, IfExp
  from ast import NamedExpr, Yield, YieldFrom, FunctionDef, Attribute, Constant, Assign, Expr
  from ast import Return, Raise, If, UnaryOp, Not, ClassDef, Nonlocal, Global, Compare, Is, Try
  from ast import Break, Continue, AsyncFunctionDef, Pass, While
  
  from scenic.core.distributions import Samplable, needsSampling, toDistribution
  from scenic.core.lazy_eval import needsLazyEvaluation
  from scenic.core.object_types import _Constructible
  import scenic.core.errors as errors
  from scenic.core.errors import (TokenParseError, PythonParseError, ASTParseError,
  								InvalidScenarioError)
  import scenic.core.dynamics as dynamics
  import scenic.core.pruning as pruning
  import scenic.syntax.veneer as veneer
  
  ### THE TOP LEVEL: compiling a Scenic program
  
  def scenarioFromString(string, params={}, model=None, scenario=None,
  					   filename='<string>', cacheImports=False):
  	"""Compile a string of Scenic code into a `Scenario`.
  
  	The optional **filename** is used for error messages."""
  	stream = io.BytesIO(string.encode())
  	return scenarioFromStream(stream, params=params, model=model, scenario=scenario,
  							  filename=filename, cacheImports=cacheImports)
  
  def scenarioFromFile(path, params={}, model=None, scenario=None, cacheImports=False):
  	"""Compile a Scenic file into a `Scenario`.
  
  	Args:
  		path (str): path to a Scenic file
  		params (dict): global parameters to override
  		model (str): Scenic module to use as world model
  		scenario (str): if there are multiple scenarios in the file, which one
  		  to use
  		cacheImports (bool): Whether to cache any imported Scenic modules.
  		  The default behavior is to not do this, so that subsequent attempts
  		  to import such modules will cause them to be recompiled. If it is
  		  safe to cache Scenic modules across multiple compilations, set this
  		  argument to True. Then importing a Scenic module will have the same
  		  behavior as importing a Python module.
  
  	Returns:
  		A `Scenario` object representing the Scenic scenario.
  	"""
  	if not os.path.exists(path):
  		raise FileNotFoundError(path)
  	fullpath = os.path.realpath(path)
  	head, extension = os.path.splitext(fullpath)
  	if not extension or extension[1:] not in scenicExtensions:
  		ok = ', '.join(scenicExtensions)
  		err = f'Scenic scenario does not have valid extension ({ok})'
  		raise RuntimeError(err)
  	directory, name = os.path.split(head)
  
  	with open(path, 'rb') as stream:
  		return scenarioFromStream(stream, params=params, model=model, scenario=scenario,
  								  filename=fullpath, path=path, cacheImports=cacheImports)
  
  def scenarioFromStream(stream, params={}, model=None, scenario=None,
  					   filename='<stream>', path=None, cacheImports=False):
  	"""Compile a stream of Scenic code into a `Scenario`."""
  	# Compile the code as if it were a top-level module
  	oldModules = list(sys.modules.keys())
  	try:
  		with topLevelNamespace(path) as namespace:
  			compileStream(stream, namespace, params=params, model=model, filename=filename)
  	finally:
  		if not cacheImports:
  			toRemove = []
  			for name, module in sys.modules.items():
  				if name not in oldModules and getattr(module, '_isScenicModule', False):
  					toRemove.append(name)
  			for name in toRemove:
  				del sys.modules[name]
  	# Construct a Scenario from the resulting namespace
  	return constructScenarioFrom(namespace, scenario)
  
  @contextmanager
  def topLevelNamespace(path=None):
  	"""Creates an environment like that of a Python script being run directly.
  
  	Specifically, __name__ is '__main__', __file__ is the path used to invoke
  	the script (not necessarily its absolute path), and the parent directory is
  	added to the path so that 'import blobbo' will import blobbo from that
  	directory if it exists there.
  	"""
  	directory = os.getcwd() if path is None else os.path.dirname(path)
  	namespace = { '__name__': '__main__' }
  	if path is not None:
  		namespace['__file__'] = path
  	sys.path.insert(0, directory)
  	try:
  		yield namespace
  	finally:
  		del sys.path[0]
  
  def compileStream(stream, namespace, params={}, model=None, filename='<stream>'):
  	"""Compile a stream of Scenic code and execute it in a namespace.
  
  	The compilation procedure consists of the following main steps:
  
  		1. Tokenize the input using the Python tokenizer.
  		2. Partition the tokens into blocks separated by import statements.
  		   This is done by the `partitionByImports` function.
  		3. Translate Scenic constructions into valid Python syntax.
  		   This is done by the `TokenTranslator`.
  		4. Parse the resulting Python code into an AST using the Python parser.
  		5. Modify the AST to achieve the desired semantics for Scenic.
  		   This is done by the `translateParseTree` function.
  		6. Compile and execute the modified AST.
  		7. After executing all blocks, extract the global state (e.g. objects).
  		   This is done by the `storeScenarioStateIn` function.
  	"""
  	if verbosity >= 2:
  		veneer.verbosePrint(f'  Compiling Scenic module from {filename}...')
  		startTime = time.time()
  	# Tokenize input stream
  	try:
  		tokens = list(tokenize.tokenize(stream.readline))
  	except tokenize.TokenError as e:
  		line = e.args[1][0] if isinstance(e.args[1], tuple) else e.args[1]
  		raise TokenParseError(line, filename, 'file ended during multiline string or expression')
  	# Partition into blocks with all imports at the end (since imports could
  	# pull in new constructor (Scenic class) definitions, which change the way
  	# subsequent tokens are transformed)
  	blocks = partitionByImports(tokens)
  	veneer.activate(params, model, filename, namespace)
  	newSourceBlocks = []
  	try:
  		# Execute preamble
  		exec(compile(preamble, '<veneer>', 'exec'), namespace)
  		namespace[namespaceReference] = namespace
  		# Execute each block
  		for blockNum, block in enumerate(blocks):
  			# Find all custom constructors defined so far (possibly imported)
  			constructors = findConstructorsIn(namespace)
  			# Translate tokens to valid Python syntax
  			startLine = max(1, block[0][2][0])
  			translator = TokenTranslator(constructors, filename)
  			newSource, allConstructors = translator.translate(block)
  			trimmed = newSource[2*(startLine-1):]	# fix up blank lines used to align errors
  			newSource = '\n'*(startLine-1) + trimmed
  			newSourceBlocks.append(trimmed)
  			if dumpTranslatedPython:
  				print(f'### Begin translated Python from block {blockNum} of {filename}')
  				print(newSource)
  				print('### End translated Python')
  			# Parse the translated source
  			tree = parseTranslatedSource(newSource, filename)
  			# Modify the parse tree to produce the correct semantics
  			newTree, requirements = translateParseTree(tree, allConstructors, filename)
  			if dumpFinalAST:
  				print(f'### Begin final AST from block {blockNum} of {filename}')
  				print(ast.dump(newTree, include_attributes=True))
  				print('### End final AST')
  			if dumpASTPython:
  				try:
  					import astor
  				except ModuleNotFoundError as e:
  					raise RuntimeError('dumping the Python equivalent of the AST'
  									   'requires the astor package')
  				print(f'### Begin Python equivalent of final AST from block {blockNum} of {filename}')
  				print(astor.to_source(newTree, add_line_information=True))
  				print('### End Python equivalent of final AST')
  			# Compile the modified tree
  			code = compileTranslatedTree(newTree, filename)
  			# Execute it
  			executeCodeIn(code, namespace)
  		# Extract scenario state from veneer and store it
  		storeScenarioStateIn(namespace, requirements)
  	finally:
  		veneer.deactivate()
  	if verbosity >= 2:
  		totalTime = time.time() - startTime
  		veneer.verbosePrint(f'  Compiled Scenic module in {totalTime:.4g} seconds.')
  	allNewSource = ''.join(newSourceBlocks)
  	return code, allNewSource
  
  ### TRANSLATION PHASE ZERO: definitions of language elements not already in Python
  
  ## Options
  
  dumpTranslatedPython = False
  dumpFinalAST = False
  dumpASTPython = False
  verbosity = 0
  usePruning = True
  
  ## Preamble
  # (included at the beginning of every module to be translated;
  # imports the implementations of the public language features)
  preamble = """\
  from scenic.syntax.veneer import *
  """
  
  ## Get Python names of various elements
  ## (for checking consistency between the translator and the veneer)
  
  api = set(veneer.__all__)
  
  ## Functions used internally
  
  createDefault = 'PropertyDefault'
  scenarioClass = 'DynamicScenario'
  behaviorClass = 'Behavior'
  monitorClass = 'Monitor'
  createTerminationAction = 'makeTerminationAction'
  internalFunctions = {
  	createDefault, scenarioClass,
  	behaviorClass, createTerminationAction,
  }
  
  # sanity check: these functions actually exist
  for imp in internalFunctions:
  	assert imp in api, imp
  
  ## Built-in functions
  
  builtinFunctions = { 'resample', 'verbosePrint', 'simulation', 'localPath' }
  
  # sanity check: implementations of built-in functions actually exist
  for imp in builtinFunctions:
  	assert imp in api, imp
  
  ## Built-in names (values which cannot be overwritten)
  
  globalParametersName = 'globalParameters'
  
  builtinNames = { globalParametersName }
  
  # sanity check: built-in names actually exist
  for name in builtinNames:
  	assert name in api, name
  
  ## Simple statements
  
  paramStatement = 'param'
  mutateStatement = 'mutate'
  
  requireStatement = 'require'
  softRequirement = 'require_soft'	# not actually a statement, but a marker for the parser
  requireAlwaysStatement = ('require', 'always')
  terminateWhenStatement = ('terminate', 'when')
  terminateSimulationWhenStatement = ('terminate', 'simulation', 'when')
  terminateAfterStatement = ('terminate', 'after')
  
  actionStatement = 'take'			# statement invoking a primitive action
  waitStatement = 'wait'				# statement invoking a no-op action
  terminateStatement = 'terminate'	# statement ending the simulation
  abortStatement = 'abort'			# statement ending a try-interrupt statement
  invokeStatement = 'do'				# statement invoking a behavior or scenario
  
  modelStatement = 'model'
  namespaceReference = '_Scenic_module_namespace'		# used in the implementation of 'model'
  
  simulatorStatement = 'simulator'
  
  oneWordStatements = {	# TODO clean up
  	paramStatement, mutateStatement, requireStatement,
  	actionStatement, waitStatement, terminateStatement,
  	abortStatement, invokeStatement, simulatorStatement
  }
  twoWordStatements = {
  	requireAlwaysStatement, terminateWhenStatement, terminateAfterStatement,
  }
  threeWordStatements = { terminateSimulationWhenStatement }
  
  threeWordIncipits = { tokens[:2]: tokens[2] for tokens in threeWordStatements } # TODO improve
  for incipit, last in threeWordIncipits.items():
  	for tok2 in twoWordStatements:
  		assert tok2 != incipit, (tok2, last)
  
  # statements implemented by functions
  functionStatements = {
  	requireStatement, paramStatement, mutateStatement,
  	modelStatement, simulatorStatement
  }
  twoWordFunctionStatements = {
  	requireAlwaysStatement, terminateWhenStatement, terminateAfterStatement,
  }
  threeWordFunctionStatements = { terminateSimulationWhenStatement }
  def functionForStatement(tokens):
  	return '_'.join(tokens) if isinstance(tokens, tuple) else tokens
  def nameForStatement(tokens):
  	return ' '.join(tokens) if isinstance(tokens, tuple) else tokens
  
  statementForImp = {
  	functionForStatement(s): nameForStatement(s)
  	for s in functionStatements | twoWordStatements
  }
  
  # sanity check: implementations actually exist
  for imp in functionStatements:
  	assert imp in api, imp
  for tokens in twoWordFunctionStatements:
  	assert len(tokens) == 2
  	imp = functionForStatement(tokens)
  	assert imp in api, imp
  for tokens in threeWordFunctionStatements:
  	assert len(tokens) == 3
  	imp = functionForStatement(tokens)
  	assert imp in api, imp
  
  # statements allowed inside behaviors
  behavioralStatements = {
  	requireStatement, actionStatement, waitStatement,
  	terminateStatement, abortStatement, invokeStatement,
  }
  behavioralImps = { functionForStatement(s) for s in behavioralStatements }
  
  # statements allowed inside scenario composition blocks
  compositionalStatements = {
  	requireStatement, waitStatement, terminateStatement, abortStatement,
  	invokeStatement,
  }
  compositionalImps = { functionForStatement(s) for s in compositionalStatements }
  
  # statements encoding requirements which need special handling
  requirementStatements = (
  	requireStatement, softRequirement,
  	functionForStatement(requireAlwaysStatement),
  	functionForStatement(terminateWhenStatement),
  )
  
  statementRaiseMarker = '_Scenic_statement_'
  
  ## Try-interrupt blocks
  
  interruptWhenStatement = ('interrupt', 'when')
  
  interruptExceptMarker = '_Scenic_interrupt_'
  
  ## Constructors and specifiers
  
  # statement defining a new constructor (Scenic class);
  # we still recognize 'constructor' for backwards-compatibility
  constructorStatements = ('class', 'constructor')
  
  Constructor = namedtuple('Constructor', ('name', 'bases'))
  
  builtinSpecifiers = {
  	# position
  	('visible', 'from'): 'VisibleFrom',
  	('offset', 'by'): 'OffsetBy',
  	('offset', 'along'): 'OffsetAlongSpec',
  	('at',): 'At',
  	('in',): 'In',
  	('on',): 'In',
  	('beyond',): 'Beyond',
  	('visible',): 'VisibleSpec',
  	('left', 'of'): 'LeftSpec',
  	('right', 'of'): 'RightSpec',
  	('ahead', 'of'): 'Ahead',
  	('behind',): 'Behind',
  	('following',): 'Following',
  
  	# heading
  	('apparently', 'facing'): 'ApparentlyFacing',
  	('facing', 'toward'): 'FacingToward',
  	('facing',): 'Facing'
  }
  
  # sanity check: implementations of specifiers actually exist
  for imp in builtinSpecifiers.values():
  	assert imp in api, imp
  
  builtinConstructors = {
  	'Point': Constructor('Point', None),
  	'OrientedPoint': Constructor('OrientedPoint', 'Point'),
  	'Object': Constructor('Object', 'OrientedPoint')
  }
  
  # sanity check: built-in constructors actually exist
  for const in builtinConstructors:
  	assert const in api, const
  
  ## Other compound statements
  
  behaviorStatement = 'behavior'		# statement defining a new behavior
  monitorStatement = 'monitor'		# statement defining a new monitor
  
  scenarioMarker = '_Scenic_scenario_'	# not a statement, but a marker for the parser
  
  # Scenario blocks
  setupBlock = 'setup'
  composeBlock = 'compose'
  scenarioBlocks = { setupBlock, composeBlock, 'precondition', 'invariant' }
  
  ## Prefix operators
  
  prefixOperators = {
  	('relative', 'position'): 'RelativePosition',
  	('relative', 'heading'): 'RelativeHeading',
  	('apparent', 'heading'): 'ApparentHeading',
  	('distance', 'from'): 'DistanceFrom',
  	('distance', 'to'): 'DistanceFrom',
  	('angle', 'from'): 'AngleFrom',
  	('angle', 'to'): 'AngleTo',
  	('ego', '='): 'ego',
  	('front', 'left'): 'FrontLeft',
  	('front', 'right'): 'FrontRight',
  	('back', 'left'): 'BackLeft',
  	('back', 'right'): 'BackRight',
  	('front', 'of'): 'Front',
  	('back', 'of'): 'Back',
  	('left', 'of'): 'Left',
  	('right', 'of'): 'Right',
  	('follow',): 'Follow',
  	('visible',): 'Visible',
  	('not', 'visible'): 'NotVisible',
  }
  assert all(1 <= len(op) <= 2 for op in prefixOperators)
  prefixIncipits = { op[0] for op in prefixOperators }
  assert not any(op in oneWordStatements for op in prefixIncipits)
  assert not any(op in twoWordStatements for op in prefixOperators)
  
  # sanity check: implementations of prefix operators actually exist
  for imp in prefixOperators.values():
  	assert imp in api, imp
  
  ## Modifiers and terminators
  
  class ModifierInfo(typing.NamedTuple):
  	name: str
  	terminators: typing.Tuple[str]
  	contexts: typing.Optional[typing.Tuple[str]] = ()
  
  modifiers = (
  	ModifierInfo('for', ('seconds', 'steps'), (invokeStatement,)),
  	ModifierInfo('until', (), (invokeStatement,)),
  )
  
  modifierNames = {}
  for mod in modifiers:
  	assert mod.name not in modifierNames, mod
  	modifierNames[mod.name] = mod
  
  terminatorsForStatements = {
  	functionForStatement(terminateAfterStatement): ('seconds', 'steps'),
  }
  
  ## Infix operators
  
  # pseudo-operator for encoding argument packages for (3+)-ary ops
  packageToken = (RIGHTSHIFT, '>>')
  packageNode = RShift
  
  class InfixOp(typing.NamedTuple):
  	syntax: str
  	implementation: typing.Optional[str]
  	arity: int
  	token: typing.Tuple[int, str]
  	node: ast.AST
  	contexts: typing.Optional[typing.Tuple[str]] = ()
  
  infixOperators = (
  	# existing Python operators with new semantics
  	InfixOp('@', 'Vector', 2, None, MatMult),
  
  	# operators not in Python (in decreasing precedence order)
  	InfixOp('at', 'FieldAt', 2, (LEFTSHIFT, '<<'), LShift),
  	InfixOp('relative to', 'RelativeTo', 2, (AMPER, '&'), BitAnd),
  	InfixOp('offset by', 'RelativeTo', 2, (AMPER, '&'), BitAnd),
  	InfixOp('offset along', 'OffsetAlong', 3, (CIRCUMFLEX, '^'), BitXor),
  	InfixOp('can see', 'CanSee', 2, (VBAR, '|'), BitOr),
  
  	# just syntactic conveniences, not really operators
  	InfixOp('from', None, 2, (COMMA, ','), None),
  	InfixOp('for', None, 2, (COMMA, ','), None, ('Follow', 'Following')),
  	InfixOp('to', None, 2, (COMMA, ','), None),
  	InfixOp('by', None, 2, packageToken, None)
  )
  
  infixTokens = {}
  infixImplementations = {}
  infixIncipits = set()
  for op in infixOperators:
  	# if necessary, set up map from Scenic to Python syntax
  	if op.token is not None:
  		tokens = tuple(op.syntax.split(' '))
  		assert 1 <= len(tokens) <= 2, op
  		assert tokens not in infixTokens, op
  		assert tokens not in twoWordStatements, op
  		infixTokens[tokens] = op
  		incipit = tokens[0]
  		assert incipit not in oneWordStatements, op
  		infixIncipits.add(incipit)
  	# if necessary, set up map from Python to Scenic semantics
  	imp = op.implementation
  	if imp is not None:
  		assert imp in api, op
  		node = op.node
  		if node in infixImplementations:	# two operators may have the same implementation
  			oldArity, oldName = infixImplementations[node]
  			assert op.arity == oldArity, (op, oldName)
  			assert imp == oldName, (op, oldName)
  		else:
  			infixImplementations[node] = (op.arity, imp)
  generalInfixOps = { tokens: op.token for tokens, op in infixTokens.items() if not op.contexts }
  
  ## Direct syntax replacements
  
  replacements = {	# TODO police the usage of these? could yield bizarre error messages
  	'of': tuple(),
  	'deg': ((STAR, '*'), (NUMBER, '0.01745329252')),
  	'ego': ((NAME, 'ego'), (LPAR, '('), (RPAR, ')')),
  	'globalParameters': ((NAME, 'globalParameters'), (LPAR, '('), (RPAR, ')')),
  }
  
  twoWordReplacements = {
  	('initial', 'scenario'): ((NAME, 'in_initial_scenario'), (LPAR, '('), (RPAR, ')')),
  }
  
  ## Illegal and reserved syntax
  
  illegalTokens = {
  	LEFTSHIFT, RIGHTSHIFT, VBAR, AMPER, TILDE, CIRCUMFLEX,
  	LEFTSHIFTEQUAL, RIGHTSHIFTEQUAL, VBAREQUAL, AMPEREQUAL, CIRCUMFLEXEQUAL,
  	DOUBLESLASH, DOUBLESLASHEQUAL
  }
  
  # sanity check: stand-in tokens for infix operators must be illegal
  for op in infixTokens.values():
  	ttype = op.token[0]
  	assert (ttype is COMMA or ttype in illegalTokens), op
  
  illegalConstructs = {
  	('async', 'def'),		# used to parse behaviors, so disallowed otherwise
  }
  
  keywords = (
  	set(constructorStatements)
  	| (oneWordStatements - {simulatorStatement})
  	| internalFunctions
  	| replacements.keys()
  )
  
  ### TRANSLATION PHASE ONE: handling imports
  
  ## Meta path finder and loader for Scenic files
  
  scenicExtensions = ('sc', 'scenic')
  
  class ScenicMetaFinder(importlib.abc.MetaPathFinder):
  	def find_spec(self, name, paths, target=None):
  		if paths is None:
  			paths = sys.path
  			modname = name
  		else:
  			modname = name.rpartition('.')[2]
  		for path in paths:
  			for extension in scenicExtensions:
  				filename = modname + '.' + extension
  				filepath = os.path.join(path, filename)
  				if os.path.exists(filepath):
  					filepath = os.path.abspath(filepath)
  					spec = importlib.util.spec_from_file_location(name, filepath,
  						loader=ScenicLoader(filepath, filename))
  					return spec
  		return None
  
  class ScenicLoader(importlib.abc.InspectLoader):
  	def __init__(self, filepath, filename):
  		self.filepath = filepath
  		self.filename = filename
  
  	def create_module(self, spec):
  		return None
  
  	def exec_module(self, module):
  		# Read source file and compile it
  		with open(self.filepath, 'r') as stream:
  			source = stream.read()
  		with open(self.filepath, 'rb') as stream:
  			code, pythonSource = compileStream(stream, module.__dict__, filename=self.filepath)
  		# Mark as a Scenic module
  		module._isScenicModule = True
  		# Save code, source, and translated source for later inspection
  		module._code = code
  		module._source = source
  		module._pythonSource = pythonSource
  
  		# If we're in the process of compiling another Scenic module, inherit
  		# objects, parameters, etc. from this one
  		if veneer.isActive():
  			veneer.currentScenario._inherit(module._scenario)
  
  	def is_package(self, fullname):
  		return False
  
  	def get_code(self, fullname):
  		module = importlib.import_module(fullname)
  		assert module._isScenicModule, module
  		return module._code
  
  	def get_source(self, fullname):
  		module = importlib.import_module(fullname)
  		assert module._isScenicModule, module
  		return module._pythonSource
  
  # register the meta path finder
  sys.meta_path.insert(0, ScenicMetaFinder())
  
  ## Miscellaneous utilities
  
  def partitionByImports(tokens):
  	"""Partition the tokens into blocks ending with import statements.
  
  	We avoid splitting top-level try-except statements, to allow the pattern of trying
  	to import an optional module and catching an ImportError. If someone tries to define
  	objects inside such a statement, woe unto them.
  	"""
  	blocks = []
  	currentBlock = []
  	duringImport = False
  	seenTry = False
  	haveImported = False
  	finishLine = False
  	parenLevel = 0
  	tokens = Peekable(tokens)
  	for token in tokens:
  		startNewBlock = False
  		if token.exact_type == LPAR:
  			parenLevel += 1
  		elif token.exact_type == RPAR:
  			parenLevel -= 1
  		if finishLine:
  			if token.type in (NEWLINE, NL) and parenLevel == 0:
  				finishLine = False
  				if duringImport:
  					duringImport = False
  					haveImported = True
  		else:
  			assert not duringImport
  			finishLine = True
  			if token.type in (DEDENT, NEWLINE, NL, COMMENT, ENCODING):
  				finishLine = False
  				if (seenTry and token.type == DEDENT and token.start[1] == 0
  					and peek(tokens).string not in ('except', 'else', 'finally')):
  					seenTry = False
  					haveImported = True 	# just in case the try contained imports
  			elif token.start[1] == 0:
  				if token.string in ('import', 'from', modelStatement):
  					duringImport = True
  				else:
  					if haveImported:
  						# could use new constructors; needs to be in a new block
  						startNewBlock = True
  					if token.string == 'try':
  						seenTry = True
  		if startNewBlock:
  			blocks.append(currentBlock)
  			currentBlock = [token]
  			haveImported = False
  		else:
  			currentBlock.append(token)
  	blocks.append(currentBlock)		# add last block
  	return blocks
  
  def findConstructorsIn(namespace):
  	"""Find all constructors (Scenic classes) defined in a namespace."""
  	constructors = []
  	for name, value in namespace.items():
  		if inspect.isclass(value) and issubclass(value, _Constructible):
  			if name in builtinConstructors:
  				continue
  			parents = []
  			for base in value.__bases__:
  				if issubclass(base, _Constructible):
  					parents.append(base.__name__)
  			constructors.append(Constructor(name, parents))
  	return constructors
  
  ### TRANSLATION PHASE TWO: translation at the level of tokens
  
  class Peekable:
  	"""Utility class to allow iterator lookahead."""
  	def __init__(self, gen):
  		self.gen = iter(gen)
  		self.current = next(self.gen, None)
  
  	def __iter__(self):
  		return self
  
  	def __next__(self):
  		cur = self.current
  		if cur is None:
  			raise StopIteration
  		self.current = next(self.gen, None)
  		return cur
  
  	def peek(self):
  		return self.current
  
  def peek(thing):
  	return thing.peek()
  
  class TokenTranslator:
  	"""Translates a Scenic token stream into valid Python syntax.
  
  	This is a stateful process because constructor (Scenic class) definitions
  	change the way subsequent code is parsed.
  	"""
  	def __init__(self, constructors=(), filename='<unknown>'):
  		self.constructors = dict(builtinConstructors)
  		for constructor in constructors:
  			name = constructor.name
  			assert name not in self.constructors
  			self.constructors[name] = constructor
  		self.filename = filename
  
  	def parseError(self, tokenOrLine, message):
  		raise TokenParseError(tokenOrLine, self.filename, message)
  
  	def createConstructor(self, name, parents):
  		parents = tuple(parents)
  		assert parents
  		self.constructors[name] = Constructor(name, parents)
  
  	def specifiersForConstructor(self, const):
  		# Currently all specifiers can be used with any constructor;
  		# I'm leaving this here in case we later allow custom specifiers to be inherited
  		return builtinSpecifiers
  		#name, parents = self.constructors[const]
  
  	def translate(self, tokens):
  		"""Do the actual translation of the token stream."""
  		tokens = Peekable(tokens)
  		newTokens = []
  		functionStack = []
  		context, startLevel = None, None
  		specifiersIndented = False
  		parenLevel = 0
  		row, col = 0, 0		# position of next token to write out
  		orow, ocol = 0, 0	# end of last token in the original source
  		startOfLine = True
  		startOfStatement = True 	# position where a (simple) statement could begin
  		constructors = self.constructors
  
  		for token in tokens:
  			ttype = token.exact_type
  			tstring = token.string
  			skip = False
  			endToken = token 	# token to advance past in column count
  			movedUpTo = False
  
  			def injectToken(tok, spaceAfter=0):
  				"""Add a token to the output stream, trying to preserve spacing."""
  				nonlocal row, col, movedUpTo
  				if not movedUpTo:
  					moveUpTo(token)
  					moveBeyond(token)
  					movedUpTo = True
  				ty, string = tok[:2]
  				if len(tok) >= 3:
  					moveBeyond(tok)
  					srow, scol = tok[2]
  					erow, ecol = tok[3]
  					width = ecol - scol
  					height = erow - srow
  				else:
  					width = len(string)
  					height = 0
  				ncol = ecol if height > 0 else col + width
  				newToken = (ty, string, (row, col), (row+height, ncol), '')
  				newTokens.append(newToken)
  				if ty in (NEWLINE, NL):
  					row += 1
  					col = 0
  				elif height > 0:
  					row += height
  					col = ncol
  				else:
  					col += width + spaceAfter
  			def moveUpTo(tok):
  				nonlocal row, col, orow, ocol
  				nrow, ncol = tok[2]
  				if nrow > orow:
  					row = nrow
  					col = ncol
  				else:
  					gap = ncol - ocol
  					assert gap >= 0, (tok, row, col, ocol)
  					col += gap
  			def moveBeyond(tok):
  				nonlocal orow, ocol
  				nrow, ncol = tok[3]
  				if nrow > orow or (nrow == orow and ncol > ocol):
  					orow = nrow
  					ocol = ncol
  			def advance(skip=True):
  				nextToken = next(tokens)
  				if skip:
  					moveBeyond(nextToken)
  				else:
  					injectToken(nextToken)
  				return nextToken
  			def callFunction(function, argument=None, implementation=None):
  				nonlocal skip, matched, functionStack
  				functionStack.append((function, parenLevel))
  				implementation = function if implementation is None else implementation
  				injectToken((NAME, implementation))
  				injectToken((LPAR, '('))
  				if argument is not None:
  					injectToken(argument)
  					injectToken((COMMA, ','))
  				skip = True
  				matched = True
  			def popFunction():
  				nonlocal context, startLevel
  				functionStack.pop()
  				injectToken((RPAR, ')'))
  				context, startLevel = (None, 0) if len(functionStack) == 0 else functionStack[-1]
  			def wrapStatementCall():
  				injectToken((NAME, 'raise'), spaceAfter=1)
  				injectToken((NAME, statementRaiseMarker), spaceAfter=1)
  				injectToken((NAME, 'from'), spaceAfter=1)
  
  			# Catch Python operators that can't be used in Scenic
  			if ttype in illegalTokens:
  				self.parseError(token, f'illegal operator "{tstring}"')
  
  			# Determine which operators are allowed in current context
  			allowedPrefixOps = prefixOperators
  			allowedInfixOps = dict()
  			allowedModifiers = dict()
  			allowedTerminators = set()
  			inConstructorContext = False
  			context, startLevel = functionStack[-1] if functionStack else (None, None)
  			if parenLevel == startLevel:
  				if context in constructors:
  					inConstructorContext = True
  					allowedPrefixOps = self.specifiersForConstructor(context)
  				else:
  					for opTokens, op in infixTokens.items():
  						if not op.contexts or context in op.contexts:
  							allowedInfixOps[opTokens] = op.token
  					for name, mod in modifierNames.items():
  						if not mod.contexts or context in mod.contexts:
  							allowedModifiers[name] = mod.name
  					if context in modifierNames:
  						allowedTerminators = modifierNames[context].terminators
  					elif context in terminatorsForStatements:
  						allowedTerminators = terminatorsForStatements[context]
  			else:
  				allowedInfixOps = generalInfixOps
  
  			# Parse next token
  			if ttype == LPAR or ttype == LSQB:		# keep track of nesting level
  				parenLevel += 1
  			elif ttype == RPAR or ttype == RSQB:	# ditto
  				parenLevel -= 1
  			elif ttype == STRING:
  				# special case for global parameters with quoted names:
  				# transform "name"=value into "name", value
  				if (len(functionStack) > 0 and functionStack[-1][0] == paramStatement
  					and peek(tokens).string == '='):
  					next(tokens)	# consume '='
  					injectToken(token)
  					injectToken((COMMA, ','))
  					skip = True
  			elif ttype == NAME:		# the interesting case: almost all new syntax falls in here
  				# try to match 2-word language constructs
  				matched = False
  				nextToken = peek(tokens)		# lookahead so we can give 2-word ops precedence
  				if nextToken is not None:
  					endToken = nextToken	# tentatively; will be overridden if no match
  					nextString = nextToken.string
  					twoWords = (tstring, nextString)
  					if startOfLine and tstring == 'for':	# TODO improve hack?
  						matched = True
  						endToken = token
  					elif startOfLine and tstring in constructorStatements:	# class definition
  						if nextToken.type != NAME or nextString in keywords:
  							self.parseError(nextToken, f'invalid class name "{nextString}"')
  						nextToken = next(tokens)	# consume name
  						bases, scenicParents = [], []
  						if peek(tokens).exact_type == LPAR:		# superclass specification
  							next(tokens)
  							nextToken = next(tokens)
  							while nextToken.exact_type != RPAR:
  								base = nextToken.string
  								if nextToken.exact_type != NAME:
  									self.parseError(nextToken, f'invalid superclass "{base}"')
  								bases.append(base)
  								if base in self.constructors:
  									scenicParents.append(base)
  								if peek(tokens).exact_type == COMMA:
  									next(tokens)
  								nextToken = next(tokens)
  							if not scenicParents and tstring != 'class':
  								self.parseError(nextToken,
  									f'Scenic class definition with no Scenic superclasses')
  						if peek(tokens).exact_type != COLON:
  							self.parseError(peek(tokens), 'malformed class definition')
  						if not bases:
  							bases = scenicParents = ('Object',)		# default superclass
  						if scenicParents:
  							self.createConstructor(nextString, scenicParents)
  						injectToken((NAME, 'class'), spaceAfter=1)
  						injectToken((NAME, nextString))
  						injectToken((LPAR, '('))
  						injectToken((NAME, bases[0]))
  						for base in bases[1:]:
  							injectToken((COMMA, ','), spaceAfter=1)
  							injectToken((NAME, base))
  						injectToken((RPAR, ')'))
  						skip = True
  						matched = True
  						endToken = nextToken
  					elif startOfLine and tstring == 'scenario':		# scenario definition
  						if nextToken.type != NAME:
  							self.parseError(nextToken, f'invalid scenario name "{nextString}"')
  						className = scenarioMarker + nextString
  						injectToken((NAME, 'async'), spaceAfter=1)
  						injectToken((NAME, 'def'), spaceAfter=1)
  						injectToken((NAME, className))
  						advance()	# consume name
  						skip = True
  						matched = True
  					elif startOfLine and tstring == behaviorStatement:		# behavior definition
  						if nextToken.type != NAME:
  							self.parseError(nextToken, f'invalid behavior name "{nextString}"')
  						injectToken((NAME, 'async'), spaceAfter=1)
  						injectToken((NAME, 'def'), spaceAfter=1)
  						skip = True
  						matched = True
  						endToken = token
  					elif startOfLine and tstring == monitorStatement:		# monitor definition
  						if nextToken.type != NAME:
  							self.parseError(nextToken, f'invalid monitor name "{nextString}"')
  						injectToken((NAME, 'async'), spaceAfter=1)
  						injectToken((NAME, 'def'), spaceAfter=1)
  						injectToken((NAME, dynamics.functionForMonitor(nextString)))
  						injectToken((LPAR, '('))
  						injectToken((RPAR, ')'))
  						advance()	# consume name
  						if peek(tokens).exact_type != COLON:
  							self.parseError(nextToken, 'malformed monitor definition')
  						skip = True
  						matched = True
  					elif twoWords in allowedPrefixOps:	# 2-word prefix operator
  						callFunction(allowedPrefixOps[twoWords])
  						advance()	# consume second word
  					elif not startOfStatement and twoWords in allowedInfixOps:	# 2-word infix operator
  						injectToken(allowedInfixOps[twoWords])
  						advance()	# consume second word
  						skip = True
  						matched = True
  					elif startOfLine and twoWords == interruptWhenStatement:
  						# special case for interrupt when
  						injectToken((NAME, 'except'), spaceAfter=1)
  						callFunction(interruptExceptMarker)
  						advance()	# consume second word
  						matched = True
  					elif startOfStatement and twoWords in threeWordIncipits:	# 3-word statement
  						advance()	# consume second word
  						endToken = advance()	# consume third word
  						thirdWord = endToken.tstring
  						expected = threeWordIncipits[twoWords]
  						if thirdWord != expected:
  							self.parseError(endToken,
  							                f'expected "{expected}", got "{thirdWord}"')
  						wrapStatementCall()
  						function = functionForStatement(twoWords + (thirdWord,))
  						callFunction(function)
  						matched = True
  					elif startOfStatement and twoWords in twoWordStatements:	# 2-word statement
  						wrapStatementCall()
  						function = functionForStatement(twoWords)
  						callFunction(function)
  						advance()   # consume second word
  						matched = True
  					elif inConstructorContext and tstring == 'with':	# special case for 'with' specifier
  						callFunction('With', argument=(STRING, f'"{nextString}"'))
  						advance()	# consume property name
  					elif startOfStatement and tstring == requireStatement and nextString == '[':
  						# special case for require[p]
  						next(tokens)	# consume '['
  						nextToken = next(tokens)
  						if nextToken.exact_type != NUMBER:
  							self.parseError(nextToken,
  								'soft requirement must have constant probability')
  						prob = nextToken.string
  						if not 0 <= float(prob) <= 1:
  							self.parseError(nextToken, 'probability must be between 0 and 1')
  						nextToken = next(tokens)
  						if nextToken.exact_type != RSQB:
  							self.parseError(nextToken, 'malformed soft requirement')
  						wrapStatementCall()
  						callFunction(softRequirement, argument=(NUMBER, prob))
  						endToken = nextToken
  					elif twoWords in twoWordReplacements:	# 2-word direct replacement
  						for tok in twoWordReplacements[twoWords]:
  							injectToken(tok, spaceAfter=1)
  						advance()	# consume second word
  						skip = True
  					elif twoWords in illegalConstructs:
  						construct = ' '.join(twoWords)
  						self.parseError(token,
  							f'Python construct "{construct}" not allowed in Scenic')
  				if not matched:
  					# 2-word constructs don't match; try 1-word
  					endToken = token
  					oneWord = (tstring,)
  					if oneWord in allowedPrefixOps:		# 1-word prefix operator
  						callFunction(allowedPrefixOps[oneWord])
  					elif not startOfStatement and oneWord in allowedInfixOps:	# 1-word infix operator
  						injectToken(allowedInfixOps[oneWord])
  						skip = True
  					elif inConstructorContext:		# couldn't match any 1- or 2-word specifier
  						self.parseError(token, f'unknown specifier "{tstring}"')
  					elif not startOfStatement and tstring in allowedModifiers:
  						injectToken((COMMA, ','))
  						callFunction(tstring, argument=(STRING, f'"{tstring}"'),
  									 implementation='Modifier')
  						skip = True
  					elif not startOfStatement and tstring in allowedTerminators:
  						injectToken((COMMA, ','))
  						injectToken((STRING, f'"{tstring}"'))
  						popFunction()
  						skip = True
  					elif startOfStatement and tstring in oneWordStatements:		# 1-word statement
  						wrapStatementCall()
  						callFunction(tstring)
  					elif token.start[1] == 0 and tstring == modelStatement:		# model statement
  						components = []
  						while peek(tokens).exact_type not in (COMMENT, NEWLINE):
  							nextToken = next(tokens)
  							if nextToken.exact_type != NAME and nextToken.string != '.':
  								self.parseError(nextToken, 'invalid module name')
  							components.append(nextToken.string)
  						if not components:
  							self.parseError(token, 'model statement is missing module name')
  						components.append("'")
  						literal = "'" + ''.join(components)
  						callFunction(modelStatement, argument=(NAME, namespaceReference))
  						injectToken((STRING, literal))
  						skip = True
  					elif startOfLine and tstring in scenarioBlocks:		# named block of scenario
  						if peek(tokens).exact_type != COLON:
  							self.parseError(peek(tokens), f'malformed "{tstring}" block')
  						injectToken((NAME, 'async'), spaceAfter=1)
  						injectToken((NAME, 'def'), spaceAfter=1)
  						injectToken((NAME, tstring))
  						injectToken((LPAR, '('))
  						injectToken((RPAR, ')'))
  						skip = True
  					elif (tstring in self.constructors
  						  and peek(tokens).exact_type not in (RPAR, RSQB, RBRACE, COMMA)):
  						# instance definition
  						callFunction(tstring)
  					elif tstring in replacements:	# direct replacement
  						for tok in replacements[tstring]:
  							injectToken(tok, spaceAfter=1)
  						skip = True
  					elif startOfLine and tstring == 'from':		# special case to allow 'from X import Y'
  						pass
  					elif tstring in keywords:		# some malformed usage
  						self.parseError(token, f'unexpected keyword "{tstring}"')
  					elif tstring in illegalConstructs:
  						self.parseError(token,
  							f'Python construct "{tstring}" not allowed in Scenic')
  					else:
  						pass	# nothing matched; pass through unchanged to Python
  
  			# Detect the end of function argument lists
  			if len(functionStack) > 0:
  				context, startLevel = functionStack[-1]
  				while parenLevel < startLevel:		# we've closed all parens for the current function
  					popFunction()
  				inConstructor = any(context in constructors for context, sl in functionStack)
  				if inConstructor and parenLevel == startLevel and ttype == COMMA:		# starting a new specifier
  					while functionStack and context not in constructors:
  						popFunction()
  					# allow the next specifier to be on the next line, if indented
  					injectToken(token)		# emit comma immediately
  					skip = True
  					nextToken = peek(tokens)
  					specOnNewLine = False
  					while nextToken.exact_type in (NEWLINE, NL, COMMENT, ENDMARKER):
  						specOnNewLine = True
  						if nextToken.exact_type == COMMENT:
  							advance(skip=False)		# preserve comment
  							nextToken = peek(tokens)
  						if nextToken.exact_type not in (NEWLINE, NL):
  							self.parseError(nextToken, 'comma with no specifier following')
  						advance(skip=False)		# preserve newline
  						nextToken = peek(tokens)
  					if specOnNewLine and not specifiersIndented:
  						nextToken = next(tokens)		# consume indent
  						if nextToken.exact_type != INDENT:
  							self.parseError(nextToken,
  								'expected indented specifier (extra comma on previous line?)')
  						injectToken(nextToken)
  						specifiersIndented = True
  				elif ttype in (NEWLINE, ENDMARKER, COMMENT, SEMI):	# end of line or statement
  					if parenLevel != 0:
  						self.parseError(token, 'unmatched parens/brackets')
  					interrupt = False
  					if functionStack and functionStack[0][0] == interruptExceptMarker:
  						lastToken = newTokens[-1]
  						if lastToken[1] != ':':
  							self.parseError(nextToken, 'expected colon for interrupt')
  						newTokens.pop()		# remove colon for now
  						interrupt = True
  					while len(functionStack) > 0:
  						functionStack.pop()
  						injectToken((RPAR, ')'))
  					if interrupt:
  						injectToken((COLON, ':'))
  
  			# Output token unchanged, unless handled above
  			if not skip:
  				injectToken(token)
  			else:
  				moveBeyond(endToken)
  			startOfLine = (ttype in (ENCODING, NEWLINE, NL, INDENT, DEDENT))
  			startOfStatement = startOfLine or (ttype == SEMI)
  
  		rewrittenSource = tokenize.untokenize(newTokens)
  		if not isinstance(rewrittenSource, str):	# TODO improve?
  			rewrittenSource = str(rewrittenSource, encoding='utf-8')
  		return rewrittenSource, self.constructors
  
  ### TRANSLATION PHASE THREE: parsing of Python resulting from token translation
  
  def parseTranslatedSource(source, filename):
  	try:
  		tree = parse(source, filename=filename)
  		return tree
  	except SyntaxError as e:
  		raise PythonParseError(e) from None
  
  ### TRANSLATION PHASE FOUR: modifying the parse tree
  
  temporaryName = '_Scenic_temporary_name'
  behaviorArgName = '_Scenic_current_behavior'
  checkInvariantsName = '_Scenic_check_invariants'
  interruptPrefix = '_Scenic_interrupt'
  
  abortFlag = Attribute(Name('BlockConclusion', Load()), 'ABORT', Load())
  breakFlag = Attribute(Name('BlockConclusion', Load()), 'BREAK', Load())
  continueFlag = Attribute(Name('BlockConclusion', Load()), 'CONTINUE', Load())
  returnFlag = Attribute(Name('BlockConclusion', Load()), 'RETURN', Load())
  finishedFlag = Attribute(Name('BlockConclusion', Load()), 'FINISHED', Load())
  
  noArgs = ast.arguments(
  	posonlyargs=[],
  	args=[], vararg=None,
  	kwonlyargs=[], kw_defaults=[],
  	kwarg=None, defaults=[])
  selfArg = ast.arguments(
  	posonlyargs=[],
  	args=[ast.arg(arg='self', annotation=None)], vararg=None,
  	kwonlyargs=[], kw_defaults=[],
  	kwarg=None, defaults=[])
  tempArg = ast.arguments(
  	posonlyargs=[],
  	args=[ast.arg(arg=temporaryName, annotation=None)], vararg=None,
  	kwonlyargs=[], kw_defaults=[],
  	kwarg=None, defaults=[Constant(None)])
  initialBehaviorArgs = [
  	ast.arg(arg=behaviorArgName, annotation=None),
  	ast.arg(arg='self', annotation=None)
  ]
  onlyBehaviorArgs = ast.arguments(
  	posonlyargs=[],
  	args=initialBehaviorArgs, vararg=None,
  	kwonlyargs=[], kw_defaults=[],
  	kwarg=None, defaults=[])
  
  class AttributeFinder(NodeVisitor):
  	"""Utility class for finding all referenced attributes of a given name."""
  	@staticmethod
  	def find(target, node):
  		af = AttributeFinder(target)
  		af.visit(node)
  		return af.attributes
  
  	def __init__(self, target):
  		super().__init__()
  		self.target = target
  		self.attributes = set()
  
  	def visit_Attribute(self, node):
  		val = node.value
  		if isinstance(val, Name) and val.id == self.target:
  			self.attributes.add(node.attr)
  		self.visit(val)
  
  class LocalFinder(NodeVisitor):
  	"""Utility class for finding all local variables of a code block."""
  	@staticmethod
  	def findIn(block, ignoreTemporaries=True):
  		lf = LocalFinder()
  		for statement in block:
  			lf.visit(statement)
  		if ignoreTemporaries:
  			names = set(name for name in lf.names if not name.startswith(temporaryName))
  		else:
  			names = lf.names
  		return names - lf.globals - lf.nonlocals
  
  	def __init__(self):
  		self.names = set()
  		self.globals = set()
  		self.nonlocals = set()
  
  	def visit_Global(self, node):
  		self.globals.update(node.names)
  
  	def visit_Nonlocal(self, node):
  		self.nonlocals.update(node.names)
  
  	def visit_FunctionDef(self, node):
  		self.names.add(node.name)
  		self.visit(node.args)
  		for decorator in node.decorator_list:
  			self.visit(decorator)
  		if node.returns is not None:
  			self.visit(node.returns)
  		# do not visit body; it's another block
  
  	def visit_Lambda(self, node):
  		self.visit(node.args)
  		# do not visit body; it's another block
  
  	def visit_ClassDef(self, node):
  		self.names.add(node.name)
  		for child in itertools.chain(node.bases, node.keywords, node.decorator_list):
  			self.visit(child)
  		# do not visit body; it's another block
  
  	def visit_Import(self, node):
  		for alias in node.names:
  			bound = alias.asname
  			if bound is None:
  				bound = alias.name
  			self.names.add(bound)
  
  	def visit_ImportFrom(self, node):
  		self.visit_Import(node)
  
  	def visit_Name(self, node):
  		if isinstance(node.ctx, (Store, Del)):
  			self.names.add(node.id)
  
  	def visit_ExceptHandler(self, node):
  		if node.name is not None:
  			self.names.add(node.name)
  		self.generic_visit(node)
  
  class ASTSurgeon(NodeTransformer):
  	def __init__(self, constructors, filename):
  		super().__init__()
  		self.constructors = set(constructors.keys())
  		self.filename = filename
  		self.requirements = []
  		self.inRequire = False
  		self.inCompose = False
  		self.inBehavior = False
  		self.inGuard = False
  		self.inTryInterrupt = False
  		self.inInterruptBlock = False
  		self.inLoop = False
  		self.usedBreakOrContinue = False
  		self.callDepth = 0
  
  	def parseError(self, node, message):
  		raise ASTParseError(node, message, self.filename)
  
  	def unpack(self, arg, expected, node):
  		"""Unpack arguments to ternary (and up) infix operators."""
  		assert expected > 0
  		if isinstance(arg, BinOp) and isinstance(arg.op, packageNode):
  			if expected == 1:
  				self.parseError(node, 'gave too many arguments to infix operator')
  			else:
  				return self.unpack(arg.left, expected - 1, node) + [self.visit(arg.right)]
  		elif expected > 1:
  			self.parseError(node, 'gave too few arguments to infix operator')
  		else:
  			return [self.visit(arg)]
  
  	def visit(self, node):
  		if isinstance(node, ast.AST):
  			return super().visit(node)
  		elif isinstance(node, list):
  			newStatements = []
  			for statement in node:
  				newStatement = self.visit(statement)
  				if isinstance(newStatement, ast.AST):
  					newStatements.append(newStatement)
  				else:
  					newStatements.extend(newStatement)
  			return newStatements
  		else:
  			raise RuntimeError(f'unknown object {node} encountered during AST surgery')
  
  	def visit_Name(self, node):
  		if node.id in builtinNames:
  			if not isinstance(node.ctx, Load):
  				self.parseError(node, f'unexpected keyword "{node.id}"')
  		return node
  
  	def visit_BinOp(self, node):
  		"""Convert infix operators to calls to the corresponding Scenic internal functions."""
  		left = node.left
  		right = node.right
  		op = node.op
  		if isinstance(op, packageNode):		# unexpected argument package
  			self.parseError(node, 'unexpected keyword "by"')
  		elif type(op) in infixImplementations:	# an operator with non-Python semantics
  			arity, impName = infixImplementations[type(op)]
  			implementation = Name(impName, Load())
  			copy_location(implementation, node)
  			assert arity >= 2
  			args = [self.visit(left)] + self.unpack(right, arity-1, node)
  			newNode = Call(implementation, args, [])
  		else:	# all other operators have the Python semantics
  			newNode = BinOp(self.visit(left), op, self.visit(right))
  		return copy_location(newNode, node)
  
  	def visit_Raise(self, node):
  		"""Handle Scenic statements encoded as raise statements.
  
  		In particular:
  		  * wrap require statements with lambdas;
  		  * handle primitive action invocations inside behaviors;
  		  * call the veneer implementations of other statements.
  		"""
  		if not isinstance(node.exc, Name) or node.exc.id != statementRaiseMarker:
  			return self.generic_visit(node)		# an ordinary raise statement
  		assert isinstance(node.cause, Call)
  		assert isinstance(node.cause.func, Name)
  		node = node.cause 	# move to inner call
  		func = node.func
  		assert isinstance(func, Name)
  		if self.inCompose and func.id not in compositionalImps:
  			statement = statementForImp[func.id]
  			self.parseError(node, f'"{statement}" cannot be used in a {composeBlock} block')
  		if self.inBehavior and func.id not in behavioralImps:
  			statement = statementForImp[func.id]
  			self.parseError(node, f'"{statement}" cannot be used in a behavior')
  		if func.id in requirementStatements:		# require, terminate when, etc.
  			if func.id == softRequirement:
  				func.id = requireStatement
  				numArgs = 2
  				if len(node.args) != 2:
  					self.parseError(node, f'"require" takes exactly 1 argument')
  			else:
  				numArgs = 1
  			self.validateSimpleCall(node, numArgs)
  			cond = node.args[-1]
  			assert not self.inRequire
  			self.inRequire = True
  			req = self.visit(cond)
  			self.inRequire = False
  			reqID = Constant(len(self.requirements), None)	# save ID number
  			self.requirements.append(req)		# save condition for later inspection when pruning
  			closure = Lambda(noArgs, req)		# enclose requirement in a lambda
  			lineNum = Constant(node.lineno, None)			# save line number for error messages
  			copy_location(closure, req)
  			copy_location(lineNum, req)
  			newArgs = [reqID, closure, lineNum]
  			if numArgs == 2:		# get probability for soft requirements
  				prob = node.args[0]
  				assert isinstance(prob, Constant) and isinstance(prob.value, (float, int))
  				newArgs.append(prob)
  			return copy_location(Expr(Call(func, newArgs, [])), node)
  		elif func.id == simulatorStatement:
  			self.validateSimpleCall(node, 1)
  			sim = self.visit(node.args[0])
  			closure = copy_location(Lambda(noArgs, sim), sim)
  			return copy_location(Expr(Call(func, [closure], [])), node)
  		elif func.id == invokeStatement:		# Sub-behavior or sub-scenario statement
  			seenModifier = False
  			invoked = []
  			args = []
  			for arg in node.args:
  				if (isinstance(arg, Call) and isinstance(arg.func, Name)
  					and arg.func.id == 'Modifier'):
  					if seenModifier:
  						self.parseError(arg,
  							f'incompatible qualifiers for "{invokeStatement}" statement')
  					seenModifier = True
  					assert len(arg.args) >= 2
  					assert isinstance(arg.args[0], Constant)
  					if arg.args[0].value == 'until':
  						arg.args[1] = Lambda(noArgs, arg.args[1])
  					args.append(self.visit(arg))
  				elif seenModifier:
  					self.parseError(arg, f'malformed "{invokeStatement}" statement')
  				else:
  					invoked.append(self.visit(arg))
  			maxInvoked = 1 if self.inBehavior and not self.inCompose else None
  			self.validateSimpleCall(node, (1, maxInvoked), onlyInBehaviors=True, args=invoked)
  			subHandler = Attribute(Name(behaviorArgName, Load()), '_invokeSubBehavior', Load())
  			subArgs = [Name('self', Load()), Tuple(invoked, Load())] + args
  			subRunner = Call(subHandler, subArgs, [])
  			return self.generateInvocation(node, subRunner, invoker=YieldFrom)
  		elif func.id == actionStatement:		# Action statement
  			if self.inCompose:
  				self.parseError(func, f'cannot use "{actionStatement}" in a {composeBlock} block')
  			self.validateSimpleCall(node, (1, None), onlyInBehaviors=True)
  			action = Tuple(self.visit(node.args), Load())
  			return self.generateInvocation(node, action)
  		elif func.id == waitStatement:		# Wait statement
  			self.validateSimpleCall(node, 0, onlyInBehaviors=True)
  			return self.generateInvocation(node, Constant((), None))
  		elif func.id == terminateStatement:		# Terminate statement
  			self.validateSimpleCall(node, 0, onlyInBehaviors=True)
  			termination = Call(Name(createTerminationAction, Load()),
  							   [Constant(node.lineno, None)], [])
  			return self.generateInvocation(node, termination)
  		elif func.id == abortStatement:		# abort statement for try-interrupt statements
  			if not self.inTryInterrupt:
  				self.parseError(node, '"abort" outside of try-interrupt statement')
  			self.validateSimpleCall(node, 0, onlyInBehaviors=True)
  			return copy_location(Return(abortFlag), node)
  		else:
  			# statement implemented directly by a function; leave call intact
  			newCall = self.visit_Call(node)
  			return copy_location(Expr(newCall), node)
  
  	def validateSimpleCall(self, node, numArgs, onlyInBehaviors=False, args=None):
  		func = node.func
  		name = func.id
  		if onlyInBehaviors and not self.inBehavior:
  			self.parseError(node, f'"{name}" can only be used in a behavior')
  		args = node.args if args is None else args
  		if isinstance(numArgs, tuple):
  			assert len(numArgs) == 2
  			low, high = numArgs
  			if high is not None and len(args) > high:
  				self.parseError(node, f'"{name}" takes at most {high} argument(s)')
  			if len(args) < low:
  				self.parseError(node, f'"{name}" takes at least {low} argument(s)')
  		elif len(args) != numArgs:
  			self.parseError(node, f'"{name}" takes exactly {numArgs} argument(s)')
  		if len(node.keywords) != 0:
  			self.parseError(node, f'"{name}" takes no keyword arguments')
  		for arg in args:
  			if isinstance(arg, Starred):
  				self.parseError(node, f'argument unpacking cannot be used with "{name}"')
  
  	def generateInvocation(self, node, actionlike, invoker=Yield):
  		"""Generate an invocation of an action, behavior, or scenario."""
  		invokeAction = Expr(invoker(actionlike))
  		checkInvariants = Expr(Call(Name(checkInvariantsName, Load()), [], []))
  		return [copy_location(invokeAction, node), copy_location(checkInvariants, node)]
  
  	def visit_Try(self, node):
  		"""Handle try-interrupt blocks."""
  		interrupts = []
  		exceptionHandlers = []
  		for handler in node.handlers:
  			ty = handler.type
  			if (isinstance(ty, Call) and isinstance(ty.func, Name)
  				and ty.func.id == interruptExceptMarker):
  				assert handler.name is None
  				if len(ty.args) != 1:
  					self.parseError(handler, '"interrupt when" takes a single expression')
  				interrupts.append((ty.args[0], handler.body))
  			else:
  				exceptionHandlers.append(handler)
  		if not interrupts:		# an ordinary try-except block
  			return self.generic_visit(node)
  
  		# Add dead copy of all interrupts to ensure all local variables defined inside
  		# the body and handlers are also defined as locals in the top-level function
  		statements = []
  		oldInTryInterrupt = self.inTryInterrupt
  		if not self.inTryInterrupt:
  			deadcopy = If(Constant(False), [node], [])
  			statements.append(deadcopy)
  			self.inTryInterrupt = True
  
  		# Construct body
  		oldInInterruptBlock, oldInLoop = self.inInterruptBlock, self.inLoop
  		self.inInterruptBlock = True
  		self.inLoop = False
  		self.usedBreakOrContinue = False
  
  		def makeInterruptBlock(name, body):
  			newBody = self.visit(body)
  			allLocals = LocalFinder.findIn(newBody)
  			if allLocals:
  				newBody.insert(0, Nonlocal(list(allLocals)))
  			newBody.append(Return(finishedFlag))
  			return FunctionDef(name, onlyBehaviorArgs, newBody, [], None)
  
  		bodyName = f'{interruptPrefix}_body'
  		statements.append(makeInterruptBlock(bodyName, node.body))
  
  		# Construct interrupt handlers and condition checkers
  		handlerNames, conditionNames = [], []
  		for i, (condition, block) in enumerate(interrupts):
  			handlerName = f'{interruptPrefix}_handler_{i}'
  			handlerNames.append(handlerName)
  			conditionName = f'{interruptPrefix}_condition_{i}'
  			conditionNames.append(conditionName)
  
  			statements.append(makeInterruptBlock(handlerName, block))
  			self.inGuard = True
  			checker = Lambda(noArgs, self.visit(condition))
  			self.inGuard = False
  			defChecker = Assign([Name(conditionName, Store())], checker)
  			statements.append(defChecker)
  
  		self.inInterruptBlock, self.inLoop = oldInInterruptBlock, oldInLoop
  		self.inTryInterrupt = oldInTryInterrupt
  
  		# Prepare tuples of interrupt conditions and handlers
  		# (in order from high priority to low, so reversed relative to the syntax)
  		conditions = Tuple([Name(n, Load()) for n in reversed(conditionNames)], Load())
  		handlers = Tuple([Name(n, Load()) for n in reversed(handlerNames)], Load())
  
  		# Construct code to execute the try-interrupt statement
  		args = [
  			Name(behaviorArgName, Load()),
  			Name('self', Load()),
  			Name(bodyName, Load()),
  			conditions,
  			handlers
  		]
  		callRuntime = Call(Name('runTryInterrupt', Load()), args, [])
  		runTI = Assign([Name(temporaryName, Store())], YieldFrom(callRuntime))
  		statements.append(runTI)
  		result = Name(temporaryName, Load())
  		if self.usedBreakOrContinue:
  			test = Compare(result, [Is()], [breakFlag])
  			statements.append(If(test, [Break()], []))
  			test = Compare(result, [Is()], [continueFlag])
  			statements.append(If(test, [Continue()], []))
  		test = Compare(result, [Is()], [returnFlag])
  		retCheck = If(test, [Return(Attribute(result, 'return_value', Load()))], [])
  		statements.append(retCheck)
  
  		# Construct overall try-except statement
  		if exceptionHandlers or node.finalbody:
  			newTry = Try(statements,
  						 [self.visit(handler) for handler in exceptionHandlers],
  						 self.visit(node.orelse),
  						 self.visit(node.finalbody))
  			return copy_location(newTry, node)
  		else:
  			return statements
  
  	def visit_For(self, node):
  		old = self.inLoop
  		self.inLoop = True
  		newNode = self.generic_visit(node)
  		self.inLoop = old
  		return newNode
  
  	def visit_While(self, node):
  		old = self.inLoop
  		self.inLoop = True
  		newNode = self.generic_visit(node)
  		self.inLoop = old
  		return newNode
  
  	def visit_FunctionDef(self, node):
  		oldInLoop, oldInInterruptBlock = self.inLoop, self.inInterruptBlock
  		self.inLoop, self.inInterruptBlock = False, False
  		newNode = self.generic_visit(node)
  		self.inLoop, self.inInterruptBlock = oldInLoop, oldInInterruptBlock
  		return newNode
  
  	def visit_Break(self, node):
  		if self.inInterruptBlock and not self.inLoop:
  			self.usedBreakOrContinue = True
  			newNode = Return(breakFlag)
  			return copy_location(newNode, node)
  		else:
  			return self.generic_visit(node)
  
  	def visit_Continue(self, node):
  		if self.inInterruptBlock and not self.inLoop:
  			self.usedBreakOrContinue = True
  			newNode = Return(continueFlag)
  			return copy_location(newNode, node)
  		else:
  			return self.generic_visit(node)
  
  	def visit_Return(self, node):
  		if self.inInterruptBlock:
  			value = Constant(None) if node.value is None else node.value
  			ret = Return(Call(returnFlag, [value], []))
  			return copy_location(ret, node)
  		else:
  			return self.generic_visit(node)
  
  	def visit_Call(self, node):
  		"""Handle Scenic syntax and semantics of function calls.
  
  		In particular:
  		  * unpack argument packages for operators;
  		  * check for iterable unpacking applied to distributions.
  		"""
  		func = node.func
  		newArgs = []
  		# Translate arguments, unpacking any argument packages
  		self.callDepth += 1
  		wrappedStar = False
  		for arg in node.args:
  			if isinstance(arg, BinOp) and isinstance(arg.op, packageNode):
  				newArgs.extend(self.unpack(arg, 2, node))
  			elif isinstance(arg, Starred) and not self.inBehavior:
  				wrappedStar = True
  				checkedVal = Call(Name('wrapStarredValue', Load()),
  								  [self.visit(arg.value), Constant(arg.value.lineno, None)],
  								  [])
  				newArgs.append(Starred(checkedVal, Load()))
  			else:
  				newArgs.append(self.visit(arg))
  		newKeywords = [self.visit(kwarg) for kwarg in node.keywords]
  		newFunc = self.visit(func)
  		self.callDepth -= 1
  		if wrappedStar:
  			newNode = Call(Name('callWithStarArgs', Load()), [newFunc] + newArgs, newKeywords)
  		else:
  			newNode = Call(newFunc, newArgs, newKeywords)
  		newNode = copy_location(newNode, node)
  		ast.fix_missing_locations(newNode)
  		return newNode
  
  	def visit_AsyncFunctionDef(self, node):
  		"""Process Scenic constructs parsed as async function definitions.
  
  		These include:
  			* scenario definitions;
  			* behavior (and monitor) definitions.
  		"""
  		if node.name.startswith(scenarioMarker):	# scenario definition
  			return self.transformScenarioDefinition(node)
  		else:
  			return self.transformBehaviorDefinition(node)
  
  	def transformScenarioDefinition(self, node):
  		if self.inCompose:
  			self.parseError(node, f'cannot define a scenario inside a {composeBlock} block')
  		if self.inBehavior:
  			self.parseError(node, 'cannot define a scenario inside a behavior')
  
  		# Set up arguments for setup block (also inherited by compose block)
  		args = node.args
  		args.args = initialBehaviorArgs + args.args
  		args = self.visit(args)
  
  		# Extract named blocks from scenario body, if any
  		simple = False	# simple scenario with no blocks
  		setup, compose = None, None
  		preconditions, invariants = [], []
  		for statement in node.body:
  			if isinstance(statement, AsyncFunctionDef):
  				if statement.name == setupBlock:
  					if setup:
  						self.parseError(statement,
  							f'scenario contains multiple "{setupBlock}" blocks')
  					setup = statement
  				elif statement.name == composeBlock:
  					if compose:
  						self.parseError(statement,
  							f'scenario contains multiple "{composeBlock}" blocks')
  					compose = statement
  				elif statement.name in ('precondition', 'invariant'):
  					if len(statement.body) != 1 or not isinstance(statement.body[0], Expr):
  						self.parseError(statement.body[0], f'malformed precondition/invariant')
  					self.inGuard = True
  					test = self.visit(statement.body[0].value)
  					self.inGuard = False
  					assert isinstance(test, ast.AST)
  					if statement.name == 'precondition':
  						preconditions.append(test)
  					else:
  						invariants.append(test)
  				else:
  					simple = True
  			else:
  				# scenario contains actual code; assume it is simple
  				simple = True
  		if simple:	# simple scenario: entire body is implicitly the setup block
  			if setup:
  				self.parseError(setup, f'simple scenario cannot have a "{setupBlock}" block')
  			if compose:
  				self.parseError(compose, f'simple scenario cannot have a "{composeBlock}" block')
  			if preconditions or invariants:
  				self.parseError(node, f'simple scenario cannot have preconditions/invariants')
  
  		# Construct compose block
  		if compose or preconditions or invariants:
  			self.inCompose = self.inBehavior = True
  			preamble = self.makeBehaviorPreamble(preconditions, invariants)
  			if compose:
  				body = self.visit(compose.body)
  				if setup:
  					# make all locals in compose block actually use scope of setup block
  					# (the compose definition will be placed inside the setup function)
  					setupLocals = LocalFinder.findIn(setup.body)
  					composeLocals = LocalFinder.findIn(body)
  					commonLocals = setupLocals & composeLocals
  					if commonLocals:
  						body.insert(0, Nonlocal(list(commonLocals)))
  			else:
  				# generate no-op compose block to ensure invariants are checked
  				wait = self.generateInvocation(node, Constant((), None))
  				body = [While(Constant(True, None), wait, [])]
  				compose = node 	# for copy_location below
  			newBody = preamble + body
  			newDef = FunctionDef('_compose', noArgs, newBody, [], None)
  			self.inCompose = self.inBehavior = False
  			compose = copy_location(newDef, compose)
  		else:
  			compose = Assign([Name('_compose', Store())], Constant(None, None))
  
  		# Construct setup block
  		if setup:
  			oldBody = setup.body
  			oldLoc = setup
  		elif simple:
  			oldBody = node.body
  			oldLoc = node
  		else:
  			oldBody = []
  			oldLoc = node
  		getLocals = Call(Name('locals', Load()), [], [])
  		returnStmt = Return(getLocals)
  		newBody = self.visit(oldBody) + [compose, returnStmt]
  		newDef = FunctionDef('_setup', args, newBody,
  							 [Name(id='staticmethod', ctx=Load())], None)
  		setup = copy_location(newDef, oldLoc)
  
  		# Assemble scenario definition
  		name = node.name[len(scenarioMarker):]
  		body = [setup]
  		newDef = ClassDef(name, [Name(scenarioClass, Load())], [], body, [])
  		return copy_location(newDef, node)
  
  	def transformBehaviorDefinition(self, node):
  		if self.inCompose:
  			self.parseError(node, f'cannot define a behavior inside a {composeBlock} block')
  		if self.inBehavior:
  			self.parseError(node, 'cannot define a behavior inside a behavior')
  
  		# process body
  		self.inBehavior = True
  		oldInLoop = self.inLoop
  		self.inLoop = False
  		preconditions = []
  		invariants = []
  		newStatements = []
  		# preserve docstring, if any
  		if (isinstance(node.body[0], Expr) and isinstance(node.body[0].value, Constant)
  		    and isinstance(node.body[0].value.value, str)):
  			docstring = [node.body[0]]
  			oldStatements = node.body[1:]
  		else:
  			docstring = []
  			oldStatements = node.body
  		# find precondition and invariant definitions
  		for statement in oldStatements:
  			if (isinstance(statement, AnnAssign)
  				and isinstance(statement.target, Name)
  				and statement.value is None
  				and statement.simple):
  				group = None
  				if statement.target.id == 'precondition':
  					group = preconditions
  				elif statement.target.id == 'invariant':
  					group = invariants
  				else:
  					self.parseError(statement, 'unknown type of behavior attribute')
  				self.inGuard = True
  				test = self.visit(statement.annotation)
  				self.inGuard = False
  				assert isinstance(test, ast.AST)
  				group.append(test)
  			else:
  				newStatement = self.visit(statement)
  				if isinstance(newStatement, ast.AST):
  					newStatements.append(newStatement)
  				else:
  					newStatements.extend(newStatement)
  
  		newBody = self.makeBehaviorPreamble(preconditions, invariants) + newStatements
  		self.inBehavior = False
  		self.inLoop = oldInLoop
  
  		# add private current behavior argument and implicit 'self' argument
  		newArgs = self.visit(node.args)
  		newArgs.args = initialBehaviorArgs + newArgs.args
  
  		# convert to class definition
  		decorators = [self.visit(decorator) for decorator in node.decorator_list]
  		genDefn = FunctionDef('makeGenerator', newArgs, newBody, decorators, node.returns)
  		classBody = docstring + [genDefn]
  		name = node.name
  		if dynamics.isAMonitorName(name):
  			superclass = monitorClass
  			name = dynamics.monitorName(name)
  		else:
  			superclass = behaviorClass
  		newDefn = ClassDef(name, [Name(superclass, Load())], [], classBody, [])
  
  		return copy_location(newDefn, node)
  
  	def makeBehaviorPreamble(self, preconditions, invariants):
  		# generate precondition checks
  		precondChecks = []
  		for precondition in preconditions:
  			call = Call(Name('PreconditionViolation', Load()),
  			            [Name(behaviorArgName, Load()), Constant(precondition.lineno, None)],
  			            [])
  			throw = Raise(exc=call, cause=None)
  			check = If(test=UnaryOp(Not(), precondition), body=[throw], orelse=[])
  			precondChecks.append(copy_location(check, precondition))
  		# generate invariant checker
  		invChecks = []
  		for invariant in invariants:
  			call = Call(Name('InvariantViolation', Load()),
  			            [Name(behaviorArgName, Load()), Constant(invariant.lineno, None)],
  			            [])
  			throw = Raise(exc=call, cause=None)
  			check = If(test=UnaryOp(Not(), invariant), body=[throw], orelse=[])
  			invChecks.append(copy_location(check, invariant))
  		defineChecker = FunctionDef(checkInvariantsName, tempArg,
  									invChecks + [Return(Name(temporaryName, Load()))], [], None)
  		spot = Attribute(Name(behaviorArgName, Load()), 'checkInvariants', Store())
  		saveChecker = Assign([spot], Name(checkInvariantsName, Load()))
  		callChecker = Expr(Call(Name(checkInvariantsName, Load()), [], []))
  		# assemble function body preamble
  		preamble = precondChecks + [defineChecker, saveChecker, callChecker]
  		return preamble
  
  	def visit_Yield(self, node):
  		if self.inCompose:
  			self.parseError(node, f'"yield" is not allowed inside a {composeBlock} block')
  		if self.inBehavior:
  			self.parseError(node, '"yield" is not allowed inside a behavior')
  		return self.generic_visit(node)
  
  	def visit_YieldFrom(self, node):
  		if self.inCompose:
  			self.parseError(node, f'"yield from" is not allowed inside a {composeBlock} block')
  		if self.inBehavior:
  			self.parseError(node, '"yield from" is not allowed inside a behavior')
  		return self.generic_visit(node)
  
  	def visit_ClassDef(self, node):
  		"""Handle Scenic constructs parsed as class definitions.
  
  		In particular:
  		  * transform Scenic class definitions into ordinary Python ones;
  		  * leave ordinary Python class definitions alone.
  		"""
  		if node.name in self.constructors:		# Scenic class definition
  			return self.transformScenicClass(node)
  		else:		# ordinary Python class
  			for base in node.bases:
  				name = None
  				if isinstance(base, Call):
  					name = base.func.id
  				elif isinstance(base, Name):
  					name = base.id
  				if name is not None and name in self.constructors:
  					self.parseError(node,
  									f'Python class {node.name} derives from Scenic class {name}')
  			return self.generic_visit(node)
  
  	def transformScenicClass(self, node):
  		"""Process property defaults for Scenic classes."""
  		newBody = []
  		for child in node.body:
  			child = self.visit(child)
  			if isinstance(child, AnnAssign):	# default value for property
  				origValue = child.annotation
  				target = child.target
  				# extract any attributes for this property
  				metaAttrs = []
  				if isinstance(target, Subscript):
  					sl = target.slice
  					if isinstance(sl, Index):	# needed for compatibility with Python 3.8 and earlier
  						sl = sl.value
  					if isinstance(sl, Name):
  						metaAttrs.append(sl.id)
  					elif isinstance(sl, Tuple):
  						for elt in sl.elts:
  							if not isinstance(elt, Name):
  								self.parseError(elt,
  									'malformed attributes for property default')
  							metaAttrs.append(elt.id)
  					else:
  						self.parseError(sl, 'malformed attributes for property default')
  					newTarget = Name(target.value.id, Store())
  					copy_location(newTarget, target)
  					target = newTarget
  				# find dependencies of the default value
  				properties = AttributeFinder.find('self', origValue)
  				# create default value object
  				args = [
  					Set([Str(prop) for prop in properties]),
  					Set([Str(attr) for attr in metaAttrs]),
  					Lambda(selfArg, origValue)
  				]
  				value = Call(Name(createDefault, Load()), args, [])
  				copy_location(value, origValue)
  				newChild = AnnAssign(
  					target=target, annotation=value,
  					value=None, simple=True)
  				child = copy_location(newChild, child)
  			newBody.append(child)
  		node.body = newBody
  		return node
  
  def translateParseTree(tree, constructors, filename):
  	"""Modify the Python AST to produce the desired Scenic semantics."""
  	surgeon = ASTSurgeon(constructors, filename)
  	tree = fix_missing_locations(surgeon.visit(tree))
  	return tree, surgeon.requirements
  
  ### TRANSLATION PHASE FIVE: AST compilation
  
  def compileTranslatedTree(tree, filename):
  	try:
  		return compile(tree, filename, 'exec')
  	except SyntaxError as e:
  		raise PythonParseError(e) from None
  
  ### TRANSLATION PHASE SIX: Python execution
  
  def executeCodeIn(code, namespace):
  	"""Execute the final translated Python code in the given namespace."""
  	exec(code, namespace)
  
  ### TRANSLATION PHASE SEVEN: scenario construction
  
  def storeScenarioStateIn(namespace, requirementSyntax):
  	"""Post-process an executed Scenic module, extracting state from the veneer."""
  
  	# Save requirement syntax and other module-level information
  	moduleScenario = veneer.currentScenario
  	factory = veneer.simulatorFactory
  	bns = gatherBehaviorNamespacesFrom(moduleScenario._behaviors)
  	def handle(scenario):
  		scenario._requirementSyntax = requirementSyntax
  		if isinstance(scenario, type):
  			scenario._simulatorFactory = staticmethod(factory)
  		else:
  			scenario._simulatorFactory = factory
  		scenario._behaviorNamespaces = bns
  	handle(moduleScenario)
  	namespace['_scenarios'] = tuple(veneer.scenarios)
  	for scenarioClass in veneer.scenarios:
  		handle(scenarioClass)
  
  	# Extract requirements, scan for relations used for pruning, and create closures
  	# (only for top-level scenario; modular scenarios will be handled when instantiated)
  	moduleScenario._compileRequirements()
  
  	# Save global parameters
  	for name, value in veneer._globalParameters.items():
  		if needsLazyEvaluation(value):
  			raise InvalidScenarioError(f'parameter {name} uses value {value}'
  									   ' undefined outside of object definition')
  	for scenario in veneer.scenarios:
  		scenario._bindGlobals(veneer._globalParameters)
  	moduleScenario._bindGlobals(veneer._globalParameters)
  
  	namespace['_scenario'] = moduleScenario
  
  def gatherBehaviorNamespacesFrom(behaviors):
  	"""Gather any global namespaces which could be referred to by behaviors.
  
  	We'll need to rebind any sampled values in them at runtime.
  	"""
  	behaviorNamespaces = {}
  	def registerNamespace(modName, ns):
  		if modName not in behaviorNamespaces:
  			behaviorNamespaces[modName] = ns
  		else:
  			assert behaviorNamespaces[modName] is ns
  			return
  		for name, value in ns.items():
  			if (isinstance(value, types.ModuleType)
  				and getattr(value, '_isScenicModule', False)):
  				registerNamespace(value.__name__, value.__dict__)
  			else:
  				# Convert values requiring sampling to Distributions
  				dval = toDistribution(value)
  				if dval is not value:
  					ns[name] = dval
  	for behavior in behaviors:
  		modName = behavior.__module__
  		globalNamespace = behavior.makeGenerator.__globals__
  		registerNamespace(modName, globalNamespace)
  	return behaviorNamespaces
  
  def constructScenarioFrom(namespace, scenarioName=None):
  	"""Build a Scenario object from an executed Scenic module."""
  	modularScenarios = namespace['_scenarios']
  	if scenarioName:
  		ty = namespace.get(scenarioName, None)
  		if not (isinstance(ty, type) and issubclass(ty, dynamics.DynamicScenario)):
  			raise RuntimeError(f'no scenario "{scenarioName}" found')
  		if ty._requiresArguments():
  			raise RuntimeError(f'cannot instantiate scenario "{scenarioName}"'
  			                   ' with no arguments') from None
  
  		dynScenario = ty()
  	elif len(modularScenarios) > 1:
  		raise RuntimeError('multiple choices for scenario to run '
  		                   '(specify using the --scenario option)')
  	elif modularScenarios and not modularScenarios[0]._requiresArguments():
  		dynScenario = modularScenarios[0]()
  	else:
  		dynScenario = namespace['_scenario']
  
  	if not dynScenario._prepared:	# true for all except top-level scenarios
  		# Execute setup block (if any) to create objects and requirements;
  		# extract any requirements and scan for relations used for pruning
  		dynScenario._prepare()
  	scenario = dynScenario._toScenario(namespace)
  
  	# Prune infeasible parts of the space
  	if usePruning:
  		pruning.prune(scenario, verbosity=verbosity)
  
  	return scenario
  