docs: def parse_general_expression(element, namespace=None, subscript_dict=None, macro_list=None):
      """
      Parses a normal expression
      # its annoying that we have to construct and compile the grammar every time...
  
      Parameters
      ----------
      element: dictionary
  
      namespace : dictionary
  
      subscript_dict : dictionary
  
      macro_list: list of dictionaries
          [{'name': 'M', 'py_name':'m', 'filename':'path/to/file', 'args':['arg1', 'arg2']}]
  
      Returns
      -------
      translation
  
      new_elements: list of dictionaries
          If the expression contains builder functions, those builders will create new elements
          to add to our running list (that will eventually be output to a file) such as stock
          initialization and derivative funcs, etc.
  
  
      Examples
      --------
      >>> parse_general_expression({'expr': 'INTEG (FlowA, -10)',
      ...                           'py_name':'test_stock',
      ...                           'subs':None},
      ...                          {'FlowA': 'flowa'}),
      ({'kind': 'component', 'py_expr': "_state['test_stock']"},
       [{'kind': 'implicit',
         'subs': None,
         'doc': 'Provides initial conditions for test_stock function',
         'py_name': 'init_test_stock',
         'real_name': None,
         'unit': 'See docs for test_stock',
         'py_expr': '-10'},
        {'py_name': 'dtest_stock_dt',
         'kind': 'implicit',
         'py_expr': 'flowa',
         'real_name': None}])
  
      """
      if namespace is None:
          namespace = {}
      if subscript_dict is None:
          subscript_dict = {}
  
      in_ops = {
          "+": "+", "-": "-", "*": "*", "/": "/", "^": "**", "=": "==", "<=": "<=", "<>": "!=",
          "<": "<", ">=": ">=", ">": ">",
          ":and:": " and ", ":or:": " or "}  # spaces important for word-based operators
  
      pre_ops = {
          "-": "-", ":not:": " not ",  # spaces important for word-based operators
          "+": " "  # space is important, so that and empty string doesn't slip through generic
      }
  
      # in the following, if lists are empty use non-printable character
      # everything needs to be escaped before going into the grammar, in case it includes quotes
      sub_names_list = [re.escape(x) for x in subscript_dict.keys()] or ['\\a']
      sub_elems_list = [re.escape(y) for x in subscript_dict.values() for y in x] or ['\\a']
      ids_list = [re.escape(x) for x in namespace.keys()] or ['\\a']
      in_ops_list = [re.escape(x) for x in in_ops.keys()]
      pre_ops_list = [re.escape(x) for x in pre_ops.keys()]
      if macro_list is not None and len(macro_list) > 0:
          macro_names_list = [re.escape(x['name']) for x in macro_list]
      else:
          macro_names_list = ['\\a']
  
      expression_grammar = r"""
      expr_type = array / expr / empty
      expr = _ pre_oper? _ (lookup_def / build_call / macro_call / call / lookup_call / parens / number / reference) _ (in_oper _ expr)?
  
      lookup_def = ~r"(WITH\ LOOKUP)"I _ "(" _ expr _ "," _ "(" _  ("[" ~r"[^\]]*" "]" _ ",")?  ( "(" _ expr _ "," _ expr _ ")" _ ","? _ )+ _ ")" _ ")"
      lookup_call = id _ "(" _ (expr _ ","? _)* ")"  # these don't need their args parsed...
      call = func _ "(" _ (expr _ ","? _)* ")"  # these don't need their args parsed...
      build_call = builder _ "(" _ arguments _ ")"
      macro_call = macro _ "(" _ arguments _ ")"
      parens   = "(" _ expr _ ")"
  
      arguments = (expr _ ","? _)*
  
      reference = id _ subscript_list?
      subscript_list = "[" _ ((sub_name / sub_element) _ ","? _)+ "]"
  
      array = (number _ ("," / ";")? _)+ !~r"."  # negative lookahead for anything other than an array
      number = ~r"\d+\.?\d*(e[+-]\d+)?"
  
      id = ( basic_id / escape_group )
      basic_id = ~r"\w[\w\d_\s\']*"IU
      escape_group = "\"" ( "\\\"" / ~r"[^\"]"IU )* "\""
      
      sub_name = ~r"(%(sub_names)s)"IU  # subscript names (if none, use non-printable character)
      sub_element = ~r"(%(sub_elems)s)"IU  # subscript elements (if none, use non-printable character)
  
      func = ~r"(%(funcs)s)"IU  # functions (case insensitive)
      in_oper = ~r"(%(in_ops)s)"IU  # infix operators (case insensitive)
      pre_oper = ~r"(%(pre_ops)s)"IU  # prefix operators (case insensitive)
      builder = ~r"(%(builders)s)"IU  # builder functions (case insensitive)
      macro = ~r"(%(macros)s)"IU  # macros from model file (if none, use non-printable character)
  
      _ = ~r"[\s\\]*"  # whitespace character
      empty = "" # empty string
      """ % {
          # In the following, we have to sort keywords in decreasing order of length so that the
          # peg parser doesn't quit early when finding a partial keyword
          'sub_names': '|'.join(reversed(sorted(sub_names_list, key=len))),
          'sub_elems': '|'.join(reversed(sorted(sub_elems_list, key=len))),
          'funcs': '|'.join(reversed(sorted(functions.keys(), key=len))),
          'in_ops': '|'.join(reversed(sorted(in_ops_list, key=len))),
          'pre_ops': '|'.join(reversed(sorted(pre_ops_list, key=len))),
          'builders': '|'.join(reversed(sorted(builders.keys(), key=len))),
          'macros': '|'.join(reversed(sorted(macro_names_list, key=len)))
      }
      
      class ExpressionParser(parsimonious.NodeVisitor):
          # Todo: at some point, we could make the 'kind' identification recursive on expression,
          # so that if an expression is passed into a builder function, the information
          # about whether it is a constant, or calls another function, goes with it.
          def __init__(self, ast):
              self.translation = ""
              self.kind = 'constant'  # change if we reference anything else
              self.new_structure = []
              self.visit(ast)
  
          def visit_expr_type(self, n, vc):
              s = ''.join(filter(None, vc)).strip()
              self.translation = s
  
          def visit_expr(self, n, vc):
              s = ''.join(filter(None, vc)).strip()
              self.translation = s
              return s
  
          def visit_call(self, n, vc):
              self.kind = 'component'
              function_name = vc[0].lower()
              arguments = [e.strip() for e in vc[4].split(",")]
              return builder.build_function_call(functions[function_name], arguments)
  
          def visit_in_oper(self, n, vc):
              return in_ops[n.text.lower()]
  
          def visit_pre_oper(self, n, vc):
              return pre_ops[n.text.lower()]
  
          def visit_reference(self, n, vc):
              self.kind = 'component'
              id_str = vc[0]
              return id_str + '()'
  
          def visit_id(self, n, vc):
              return namespace[n.text.strip()]
  
          def visit_lookup_def(self, n, vc):
              """ This exists because vensim has multiple ways of doing lookups.
              Which is frustrating."""
              x_val = vc[4]
              pairs = vc[11]
              mixed_list = pairs.replace('(', '').replace(')', '').split(',')
              xs = mixed_list[::2]
              ys = mixed_list[1::2]
              string = "functions.lookup(%(x)s, [%(xs)s], [%(ys)s])" % {
                  'x': x_val,
                  'xs': ','.join(xs),
                  'ys': ','.join(ys)
              }
              return string
  
          def visit_array(self, n, vc):
              if 'subs' in element and element['subs']:  # first test handles when subs is not defined
                  coords = utils.make_coord_dict(element['subs'], subscript_dict, terse=False)
                  dims = [utils.find_subscript_name(subscript_dict, sub) for sub in element['subs']]
                  shape = [len(coords[dim]) for dim in dims]
                  if ';' in n.text or ',' in n.text:
                      text = n.text.strip(';').replace(' ', '').replace(';', ',')
                      data = np.array([float(s) for s in text.split(',')]).reshape(shape)
                  else:
                      data = np.tile(float(n.text), shape)
                  datastr = np.array2string(data, separator=',').replace('\n', '').replace(' ', '')
                  return textwrap.dedent("""\
                      xr.DataArray(data=%(datastr)s,
                                   coords=%(coords)s,
                                   dims=%(dims)s )""" % {
                      'datastr': datastr,
                      'coords': repr(coords),
                      'dims': repr(dims)})
  
              else:
                  return n.text.replace(' ', '')
  
          def visit_subscript_list(self, n, vc):
              refs = vc[2]
              subs = [x.strip() for x in refs.split(',')]
              coordinates = utils.make_coord_dict(subs, subscript_dict)
              if len(coordinates):
                  return '.loc[%s]' % repr(coordinates)
              else:
                  return ' '
  
          def visit_build_call(self, n, vc):
              call = vc[0]
              arglist = vc[4]
              self.kind = 'component'
  
              builder_name = call.strip().lower()
              name, structure = builders[builder_name](element, subscript_dict, arglist)
              self.new_structure += structure
  
              if builder_name == 'delay fixed':
                  warnings.warn("Delay fixed only approximates solution, may not give the same "
                                "result as vensim")
  
              return name
  
          def visit_macro_call(self, n, vc):
              call = vc[0]
              arglist = vc[4]
              self.kind = 'component'
              py_name = utils.make_python_identifier(call)[0]
              macro = [x for x in macro_list if x['py_name'] == py_name][0]  # should match once
              name, structure = builder.add_macro(macro['py_name'], macro['file_name'],
                                                  macro['params'], arglist)
              self.new_structure += structure
              return name
  
          def visit_arguments(self, n, vc):
              arglist = [x.strip(',') for x in vc]
              return arglist
  
          def visit__(self, n, vc):
              """ Handles whitespace characters"""
              return ''
  
          def visit_empty(self, n, vc):
              return 'None'
  
          def generic_visit(self, n, vc):
              return ''.join(filter(None, vc)) or n.text
  
      parser = parsimonious.Grammar(expression_grammar)
      tree = parser.parse(element['expr'])
      parse_object = ExpressionParser(tree)
  
      return ({'py_expr': parse_object.translation,
               'kind': parse_object.kind,
               'arguments': ''},
              parse_object.new_structure)