"""
Takes the tree resulting from parsing and checks that it is valid.

        * Semantic Analysis:
        * * While worklist non empty:
        * * * Scan the file and find all names it defines, adding stubs
        * * * Add any new imports to worklist
        * * For each imported file:
        * * * Check each function def'n for correctness
        * * * Create executable AST
        * * * Add to global function table
"""
from os.path import join, isdir, exists, sep, dirname
import sys
import parse, ast, value, error, stdlib
from datastruct import ConsList, BaseList, AssocList
from misc import augment
from lexer import DUMMY_LOC
from value import W_UNITLESS

NS_SEP=":"

# ___________________________________________________________________________
# Overall flow

def semantic(interp, parse_tree):
    root_package = construct_import_graph(interp, parse_tree)
    root_package.modules_do(create_top_level_names)
    root_package.modules_do(merge_open_ended_imports)
    root_package.modules_do(create_inner_funcs)
    root_package.modules_do(final_resolution)
    root_package.func_items_do(lambda fi: process_func_item(interp, fi))
    main_module = root_package.names["main"]
    assert main_module
    try:
        main_func = main_module.names["main"].as_w_func(DUMMY_LOC)
    except KeyError:
        raise error.NoMainFunction(DUMMY_LOC)
    return main_func
    
# ___________________________________________________________________________
# Utilities

def of_type(lst, typ):
    return [a for a in lst if a.type == typ]

def is_rel_id(ast):
    if ast.ns:
        return is_rel_id(ast.ns)
    return ast.id

def is_simple_id(ast):
    return not ast.ns and ast.id

def absolute_id(ast, rel_to, sep=NS_SEP):
    "Returns an absolute ID to the ID described in 'ast'."

    if not ast.ns and not ast.id:
        return ""     # reference to the root
    
    if ast.ns:
        ns_id = absolute_id(ast.ns, rel_to, sep)
    else:
        ns_id = rel_to

    return ns_id + sep + ast.id
    
def make_id(id_str, loc=DUMMY_LOC):
    "Constructs ast nodes from id_str"
    if not id_str: return None
    if id_str == NS_SEP: return parse.Ast('id', loc, ns=None, id="")
    before, col, after = id_str.rpartition(NS_SEP)
    ns = make_id(before+col)
    return parse.Ast('id', loc, ns=ns, id=after)

STD_MODULE = ":std"

# ___________________________________________________________________________
# Import and Top-Level-Definition Phase
#
# The primary data structure is called the declaration tree.  It
# contains entries for: packages (i.e., directories), modules (i.e.,
# .axel files), functions (both top-level and nested), and units.
# They are all represented by some instance of Container.
#
# Note that each Container instance has a pointer to its parent, but
# also a parent to its parent_scope.  The parent is the logical
# containing module, but the *parent_scope* is where to proceed in
# name lookup.  Generally, this terminates at the module.  So, for
# example, an identifier defined in a nested function would match
# against identifiers defined in its parent function or in the module,
# but not in the package.

class Container(object):
    """
    Simple base class that represents a hierarchical naming scope.
    Each node has a parent container (except the root), and a mapping
    from names to other containers.  Note that the items in a
    Container C's name table are usually children of C, but don't have
    to be (due to open-ended imports).
    """

    def __init__(self, name):
        self.parent = None        # set in add_child()
        self.name = name          # string
        self.names = {}           # string -> Container

    @property
    def rel_name(self):
        return self.name.rpartition(NS_SEP)[2]

    def add_child(self, item):
        assert not item.parent # does not already belong to someone
        assert isinstance(item, Container)
        self.names[item.rel_name] = item
        item.parent = self

    def root_package(self):
        if self.parent: return self.parent.root_package()
        return self

    def resolve(self, id_node):
        "Resolves an identifier AST node against the declaration tree"
        return self.resolve_str(
            id_node.loc, absolute_id(id_node, self.name))

    def resolve_str(self, loc, id_str):
        """
        Resolves an identifier path against this container.  The path
        should be either relative (a:b:c), in which it is interpreted
        relative to self, or absolute (:a:b:c).  Note that "" names self.
        """

        # check for ""
        if not id_str:
            return self

        # convert absolute references into relative ones:
        if id_str.startswith(NS_SEP):
            return self.root_package().resolve_str(loc, id_str[len(NS_SEP):])

        # extract first name
        rel_id, sep, remainder = id_str.partition(NS_SEP)

        # look up rel_id in myself or any parent scope
        scope = self
        while scope:
            if rel_id in scope.names:
                return scope.names[rel_id].resolve_str(loc, remainder)
            scope = scope.parent_scope()

        raise error.UnknownIdentifier(loc, id_str)


    def as_unit(self, loc):
        raise error.ExpectedUnit(loc)

    def as_w_value(self, loc):
        raise error.ExpectedValue(loc)

    def as_w_func(self, loc):
        raise error.ExpectedFuncValue(loc)

    def assert_func_item(self, loc):
        raise error.ExpectedFuncValue(loc)

    def parent_scope(self):
        """
        If a name is not found in this scope, returns another scope to
        check.  Generally, this terminates at the file boundary.
        """
        return None

    def func_items_do(self, func):
        for item in self.names.values():
            if item.parent is self:
                item.func_items_do(func)
        
class Package(Container):

    def __repr__(self):
        if not self.name:
            return "[Root Package]"
        else:
            return "[Package %s]" % (self.name,)
        
    def modules_do(self, func):    
        for n in self.names.values():
            assert n is not self
            if hasattr(n, 'modules_do'): n.modules_do(func)

class Module(Container):
    
    def __init__(self, name, parse_tree):
        Container.__init__(self, name)
        self.parse_tree = parse_tree         # 
        self.imports = []      # other modules imported by this Module
        self.pre_index = None    # rank in a pre-order walk, set during creation

    def __repr__(self):
        return "[Module %s]" % (self.name,)
        
    def modules_do(self, func):
        func(self)

    def func_named(self, loc, name):
        try:
            return self.names[name].assert_func_item(loc)
        except KeyError:
            res = FuncItem(loc, self.name+NS_SEP+name, None)
            self.add_child(res)
            return res

class UnitItem(Container):
    """
    A Unit is created for each unit declaration or definition in the
    user program.
    """
    def __init__(self, loc, name, w_unit):
        Container.__init__(self, name)
        self.loc = loc
        self.w_unit = w_unit
        
    def as_unit(self, loc):
        return self.w_unit

    def as_w_value(self, loc):
        return self.w_unit

    def merge(self, item):
        "Merge the definitions of self and item and return new value for self"
        if self is item:
            return self
        raise error.CannotMerge(self.loc, item.loc, self.name)

    def __repr__(self):
        return "[Unit %s]" % (self.name,)
        
class FuncItem(Container):
    """
    A Func is created for each *set of defs with the same function
    name* in the module.  The 'definitions' list contains a list of
    FuncDef instances.  During an open-ended import, the definitions
    of two Funcs are sometimes merged.
    """
    def __init__(self, loc, name, w_func_value):
        Container.__init__(self, name)
        self.loc = loc
        self.definitions = []
        if not w_func_value:
            self.w_func_value = value.W_ValueMultiFunction([])
        else:
            self.w_func_value = w_func_value

    def assert_func_item(self, loc):
        return self

    def as_w_func(self, loc):
        return self.w_func_value

    def as_w_value(self, loc):
        return self.w_func_value

    def func_items_do(self, func):
        func(self)
                
    def merge(self, item):
        "Merge the definitions of self and item and return new value for self"
        if self is item:
            return self
        if isinstance(item, FuncItem):
            item.definitions.extend(self.definitions)
            return item
        raise error.CannotMerge(self.loc, item.loc, self.name)

    def __repr__(self):
        return "[Func %s]" % (self.name,)
        
class FuncDef(object):
    """
    Added to Func.definitions for each def in the source code.  Each
    FuncDef has a module and parse_tree, but also a set of names for inner
    functions.  Note that each inner function is a Func, and has as
    its parent the container of this FuncDef.
    """
    def __init__(self, module, parse_tree):
        self.module = module
        self.parse_tree = parse_tree
        self.names = {}

    def inner_func(self, loc, name, func_item):
        try:
            return self.names[name].assert_func_item()
        except KeyError:
            self.names[name] = res = FuncItem(loc, name, None)
            res.parent = func_item
            return res

    def __cmp__(self, funcdef):
        """A funcdef is greater than another if it's pattern has
        higher priority."""
        
        if self.module is funcdef.module:
            # Within a module, whichever comes first has higher
            # priority.  Therefore, if our location is less than
            # theirs (ie, comes first), then we ourselves are greater.
            # Note that two cannot occur on the same line.
            assert self.parse_tree.loc != funcdef.parse_tree.loc
            return -cmp(self.parse_tree.loc, funcdef.parse_tree.loc)

        # Priority between two distinct modules A and B depends on the
        # imports.  If:
        #
        # A imports B but B does not import A: A has higher priority
        #
        # B imports A but A does not import B: B has higher priority
        #
        # Otherwise: whichever appears first on a pre-order walk
        # starting from the Main module has highest priority.
        def add_imports(mod, set):
            if not mod in set:
                set.add(mod)
                for m in mod.imports: add_imports(m, set)
            return set
        self_set = add_imports(self.module, set())
        funcdef_set = add_imports(funcdef.module, set())

        if funcdef.module in self_set: # A imports B
            if not self.module in funcdef_set: # but not B, A:
                return 1 # A has greater priority
        elif self.module in funcdef_set: # B imports A, but not A, B:
            return -1 # B has greater priority

        # Whichever module comes first in pre walk has higher
        # priority.  i.e., priority is HIGH if index is LOW.
        return -cmp(self.module.pre_index, funcdef.module.pre_index)

# ___________________________________________________________________________
# Synthesize Standard Library

def synthesize_std(root_package):
    std_module = Module(STD_MODULE, None)
    root_package.add_child(std_module)

    for var_nm in dir(stdlib):
        var_val = getattr(stdlib, var_nm)
        if isinstance(var_val, value.W_ValueMultiFunction):
            std_module.add_child(FuncItem(
                DUMMY_LOC, var_val.expose_as, var_val))
        elif isinstance(var_val, value.W_Unit):
            if var_nm.startswith("w_"):
                var_nm = var_nm[2:]
            std_module.add_child(UnitItem(
                DUMMY_LOC, var_nm, var_val))

# ___________________________________________________________________________
# Construct Import Graph
#
# The first thing we do is resolve and parse every module which they
# reference.  The return value of this function is an instance of
# Package called the root_package, which contains a tree of Modules
# and Packages.  The modules have no children defined yet, but the
# imports array is loaded with the modules that they themselves
# imported, in the same order as they appeared in the source text.
# The order is important later for resolving priorities of functions.

def construct_import_graph(interp, initial_parse_tree):
    # Create the initial packages/modules:
    root_package = Package("")
    synthesize_std(root_package)
    main_module = Module(':main', initial_parse_tree)
    root_package.add_child(main_module)

    # Variables used while processing imports:
    process_stack = []
    packages = {'':root_package}
    modules = dict((m.name, m) for m in root_package.names.values())
    worklist = [main_module]

    counter = 0

    def process(module):
        "Find the imports of each module."
        prog = module.parse_tree
        process_stack.append(module)

        if interp.options.dump_parse_trees:
            sys.stderr.write(prog.tree_repr())

        # _________________________________________________________________
        # Imports

        def find_loaded_module(id):
            """
            Resolves a imported identifier, finding any already-loaded
            module that contains the imported item.
            """
            
            # Get an absolute name like :a:b:c:d
            id_str = absolute_id(id, module.parent.name)
            
            # Work backwards looking for a module with that name
            while id_str:
                if id_str in modules:
                    return modules[id_str]
                id_str, _, _ = id_str.rpartition(':')

            return None

        def create_package(pkg_name):
            """
            Creates/returns a package for pkg_name and any ancestors,
            which should be an absolute name like :a:b:c:d.
            """
            
            if pkg_name in packages:
                return packages[pkg_name]
            par_pkg_name, _, _ = pkg_name.rpartition(NS_SEP)
            par_pkg = create_package(par_pkg_name)
            pkg = Package(pkg_name)
            par_pkg.add_child(pkg)
            return par

        def load_module_source(id):
            """
            Finds a file name containing the source for a module named
            'id' and creates a new Module object containing it, and
            also any Packages required.  Returns None if no such file
            can be found.
            """
            
            # Get a relative file name like "a/b/c" and a set of directories
            rel_file_nm = absolute_id(id, "", sep=sep)
            assert rel_file_nm.startswith(sep)
            rel_file_nm = rel_file_nm[len(sep):]
            
            if is_rel_id(id):
                src_dirs = [dirname(prog.loc.file_name)]
                pkg_name = module.parent.name
            else:
                src_dirs = interp.src_dirs
                pkg_name = root_package.name
                
            # Search the directories
            for src_dir in src_dirs:
                full_file_nm = join(src_dir, rel_file_nm) + interp.FILE_EXT
                if exists(full_file_nm) and not isdir(full_file_nm):

                    # crate the module:
                    imp_module_name = absolute_id(id, pkg_name)
                    imp_parse_tree = parse.parse(full_file_nm)
                    imp_module = Module(imp_module_name, imp_parse_tree)
                    modules[imp_module_name] = imp_module
                    
                    # find/create the package for this module and add it:
                    imp_pkg = create_package(pkg_name)
                    imp_pkg.add_child(imp_module)
                    
                    return imp_module

            return None

        def load_best_module_source(orig_id):
            """
            Given an import a:b:c...:z, finds the longest module a:b:c
            which can be imported.  Currently, we do not allow
            packages to be imported, so if we end up with a name a:b:c
            which names a package, we treat that as a module not found
            error.  Note that a single name can be either a module or
            a package but not both.
            """
            def helper(id):
                # Check for an existing package (we know there is no module)
                id_str = absolute_id(id, module.parent.name)
                if id_str in packages:
                    return None
                imp_module = load_module_source(id)
                if imp_module: return imp_module
                if id.ns: return helper(id.ns)
                return None

            imp_module = helper(orig_id)
            if not imp_module:
                raise error.ImportNotFound(
                    orig_id.loc,
                    absolute_id(orig_id, module.parent.name))
            return imp_module
        
        def load_import(loc, id):
            """
            Finds the module containing the import of the name id,
            parses and creates it if necessary, and adds it to the
            list of imports from the current module.
            """
            imp_module = find_loaded_module(id)
            if not imp_module:
                imp_module = load_best_module_source(id)
                worklist.append(imp_module)
            module.imports.append(imp_module)
            return imp_module

        # Process any imports, loading them into our name table:
        for impdecl in of_type(prog.decls, 'ImportDecl'):
            # XXX check that ids in a module name are always legal file names
            load_import(impdecl.loc, impdecl.id)

        # Load implied imports, both from absolute refs, and std:
        for node in prog.descendants(not_of_type=['ImportDecl']):
            if node.type == 'Id' and not is_rel_id(node):
                load_import(node.loc, node)
        load_import(DUMMY_LOC, make_id(STD_MODULE))
    
    while worklist:
        process(worklist.pop())

    # Number the modules in pre-order, which is sometimes used to
    # disambiguate priorities:
    def assign_pre_index(module, pre_counter):
        if module.pre_index is not None:
            return pre_counter
        module.pre_index = pre_counter
        pre_counter += 1
        for mod in module.imports:
            pre_counter = assign_pre_index(mod, pre_counter)
        return pre_counter
    assign_pre_index(main_module, 0)
        
    return root_package
        
# ___________________________________________________________________________
# Top-Level Functions and Units
#
# We now iterate through each module and create entries for the functions
# and units they define, as well as the names of any modules they import.
# Open-ended imports are not processed yet, that occurs in the next stage.

def create_top_level_names(module):
    if not module.parse_tree: return # modules defined in Python

    for unit_decl in of_type(module.parse_tree.decls, 'UnitDecl'):
        # supertypes resolved in final_resolution phase
        w_unit = value.unit_atom_w(unit_decl.name)
        module.add_child(UnitItem(unit_decl.loc, unit_decl.name, w_unit))

    for unit_decl in of_type(module.parse_tree.decls, 'UnitDef'):
        # actual value resolved in final_resolution phase
        module.add_child(UnitItem(unit_decl.loc, unit_decl.name, W_UNITLESS))

    for func_decl in of_type(module.parse_tree.decls, 'FuncDecl'):
        # inner funcs resolved in create_inner_funcs, non-simple-ids in
        # final_resolution
        if not is_simple_id(func_decl.id): continue
        func_item = module.func_named(func_decl.loc, func_decl.id.id)
        func_item.definitions.append(FuncDef(module, func_decl))

# ___________________________________________________________________________
# Resolve Imported Names
    
def merge_open_ended_imports(module):

    # Load an imported item into our namespace.  If we already
    # have something there, it must be mergable.
    def load_item(item):
        if item.rel_name in module.names:
            new_val = module.names[item.rel_name].merge(item)
            module.names[item.rel_name] = new_val
        else:
            module.names[item.rel_name] = item

    def load_items_from_import(open_ended, id):
        item = module.parent.resolve(id)
        if open_ended:
            assert isinstance(item, Module)
            for item in item.names.values(): load_item(item)
        else:
            load_item(item)

    if not module.parse_tree: return
    for impdecl in of_type(module.parse_tree.decls, 'ImportDecl'):
        load_items_from_import(impdecl.open_ended, impdecl.id)
    load_items_from_import(True, make_id(STD_MODULE))
    
# ___________________________________________________________________________
# Creation of inner functions
#
# We can now create the FuncItem nodes for inner funcs; we chose not
# to do create them before because we didn't know what parent to use
# yet.

def create_inner_funcs(module):
    def find_func_def(func_item, func_def, parse_tree_parent):
        assert func_def.module is module
        for _, parse_trees in parse_tree_parent.children():
            for node in parse_trees:
                if node.type == 'FuncDef':
                    if not is_simple_id(parse_tree.id):
                        raise error.NestedFuncsMustBeUnqualified(node.id.loc)
                    inner_func_item = func_def.inner_func(node.id.id, func_item)
                    inner_func_def = FuncDef(module, node)
                    inner_func_item.definitions.append(inner_func_def)
                    find_func_def(inner_func_item, inner_func_def, node)
                else:
                    find_func_def(func_item, func_def, node)

    for func_item in module.names.values():
        if not isinstance(func_item, FuncItem): continue
        for func_def in func_item.definitions:
            # process only defns from this module, the others will be
            # processed eventually
            if func_def.module is module:
                find_func_def(func_item, func_def, func_def.parse_tree)

# ___________________________________________________________________________
#
# Finally, we can take care of the last few lingering tasks in the
# global environment before we process the func definitions
# themselves.  First, we process the supertypes for units.  Second, we
# can add any externally defined inner-function definitions.
    
def as_fixed_unit(module, uniteq):
    unit_num = value.W_Unit.mul(
        module.resolve(u.id).as_unit(u.id.loc) for u in uniteq.num)
    unit_denom = value.W_Unit.mul(
        module.resolve(u_id).as_unit(u.id.loc) for u in uniteq.denom)
    return unit_num / unit_denom

def as_fixed_atom(module, uniteq):
    unit = as_fixed_unit(module, uniteq)
    if len(unit.denom) != 0 or len(unit.num) != 1:
        raise error.ExpectedUnitAtom(uniteq.loc, unit)
    return unit.num.hd

def final_resolution(module):
    if not module.parse_tree: return # module defined in Python

    runit = lambda u: as_fixed_unit(module, u)

    for unit_decl in of_type(module.parse_tree.decls, 'UnitDecl'):
        super_atoms = ConsList.from_iterable(
            as_fixed_atom(module, u) for u in unit_decl.super_units)
        w_unit = module.names[unit_decl.name].as_unit(unit_decl.loc)
        assert w_unit.is_atom()
        w_unit.num.hd.set_super_atoms(super_atoms)

    for unit_def in of_type(module.parse_tree.decls, 'UnitDef'):
        defn = as_fixed_unit(module, unit_def.defn)
        module.names[unit_def.name].w_unit = defn

    # XXX this is wrong if these externally-defined funcs create
    # nested funcs of their own!  We really need to do these in
    # stages or something.  Yuck!
    for func_def in of_type(module.parse_tree.decls, 'FuncDef'):
        if is_simple_id(func_def.id): continue # already processed
        func_item = module.resolve(func_def.id).assert_func_item()
        func_def = FuncDef(module, func_def)
        func_item.definitions.append(func_def)
        
# ___________________________________________________________________________
# Checking patterns
#
# Methods used when compiling unit patterns.  These augment the ast.Unit
# subclasses.

@augment(ast.Unit)
def unit_var(self):
    """
    Should return self is self is a unit var, or None otherwise.  In
    some cases, it may raise an error if a unit var is not allowed in
    a particular position in a pattern.
    """
    raise NotImplemented

@augment(ast.UnitFixed)
def in_pattern(self):
    return None

@augment(ast.UnitRaisedInf)
def in_pattern(self):
    self.unit.in_pattern()

@augment(ast.UnitVar)        
def in_pattern(self):
    raise error.UnitVarNotAllowedHere(d.loc)

@augment(ast.Expr)
def in_pattern(self):
    return 

@augment(ast.ExprUnitEq)
def in_pattern(self):
    for node in self.num:
        node.in_pattern()
    for node in self.denom:
        node.in_pattern()

# ___________________________________________________________________________
# Constant Data
#
# Converts parse trees of constant data into wrapped or unwrapped
# versions.

class ConstResolver(parse.Visitor):

    def PosInt(self, node):
        text = filter(lambda c: c != '_', node.int)
        return int(text)

    def Float(self, node):
        text = filter(lambda c: c != '_', node.num)
        return float(text)

    def String(self, node):
        return node.value[1:-1]

    def Dict(self, node):
        kv_parse_tree = [(node.key, node.value) for node in node.args]
        kv_val = [map(as_const, kv) for kv in kv_parse_tree]
        return dict(kv_val)

    def Tuple(self, node):
        return tuple([as_const(a) for a in node.args])

as_const = ConstResolver().proc

class ValueResolver(parse.Visitor):

    def PosInt(self, node):
        return value.W_ValueInt(as_const(node))

    def Float(self, node):
        return value.W_ValueFloat(as_const(node))

    def String(self, node):
        return value.W_ValueString(as_const(node))

    def Dict(self, node):
        return value.W_ValueDict(as_const(node))

    def List(self, node):
        return value.W_ValueList(as_const(node))

as_w_value = ValueResolver().proc

# ___________________________________________________________________________
# Function Definition Phase

class Symbol(object):
    " Any symbol "
    
    def as_unit_var(self, loc):
        raise error.ExpectedUnit(node.loc)        
    
class ValueVarSymbol(Symbol):
    " Symbols representing variables "

    def as_expr(self, loc):
        return ast.ExprLocalVar(loc, self.name)

class LocalVarSymbol(ValueVarSymbol):
    " Symbol representing a plain old local variable "
    def __init__(self, name):
        self.name = name

class NestedFuncSymbol(ValueVarSymbol):
    " Symbol representing a nested function "
    def __init__(self, name, w_func):
        self.name = name
        self.w_func = w_func
    
class UnitVarSymbol(Symbol):
    " Symbol representing a unit variable "
    def __init__(self, name):
        self.name = name
        
    def as_expr(self, loc):
        return ast.ExprUnitEq(loc, [self.as_unit_var()], [])

    def as_unit_var(self, loc):
        return ast.UnitVar(loc, self.name)
    
class FuncResolver(parse.Visitor):

    def __init__(self, symbols, func_def, func_item):
        # * symbols: an AssocList of variables defined in parent
        # scopes.  Used to resolve relative identifiers in preference
        # to the containing module.
        #
        # * func_item: the Item describing the function whose
        # definition we are processing.
        self.symbols = symbols
        self.defining_var = False
        self.func_def = func_def
        self.parent = func_def.module

        # Add any items from our node into our scope (should all be
        # nested function definitions).  This prevents local variables
        # from being created that alias these functions.
        for nm, val in func_item.names.items():
            sym = NestedFuncSymbol(nm, val.as_w_func(DUMMY_LOC))
            self.add_symbol(DUMMY_LOC, sym)

    def add_symbol(self, loc, symbol):
        if symbol.name in self.symbols:
            raise error.DuplicateVariable(loc, symbol.name)
        self.symbols = self.symbols.with_key_value(symbol.name, symbol)

    def lookup(self, name):
        try:
            return self.symbols[name]
        except KeyError:
            return None

    def FuncDecl(self, node):
        "top-level func def'n"
        name = absolute_id(node.id, self.parent.name)

        uarg_bounds = {}
        for uarg_node in node.unit_args:
            self.add_symbol(uarg_node.loc, UnitVarSymbol(uarg_node.name))
            bounds = self.proc_all(uarg_node.super_units)
            uarg_bounds[uarg_node.name] = bounds

        self.defining_var = True
        args = self.proc_all(node.args)
        self.defining_var = False

        body = self.proc(node.body)
        return ast.FunctionAst(node.loc, name, uarg_bounds, args, body)

    def StmtFuncDecl(self, node):
        "nested func def'n"
        func_decl = node.decl

        # Extract the function name, which must be simple
        if func_decl.id.ns is not None:
            raise errors.NestedFuncsMustBeUnqualified(node.id.loc)
        func_name = func_decl.id.id
        
        # Multi-function object should be in sym. table from the
        # module tree
        func_item = self.func_def.names[func_name]
        w_func = func_item.as_w_func(node.loc)

        # Process the function and get its AST
        func_ast = FuncResolver(self.symbols, func_item).proc(func_decl)
        w_func.add_func(value.AstFunction(func_ast))

        # Create an ast node like "x = y" where y is a lambda.  At
        # execution time, the ExprLambda will handle the dynamic
        # scoping.
        return ast.StmtAssign(
            node.loc,
            ast.Arg(node.loc, True, func_name, None),
            ast.ExprLambda.new(loc=node.loc, w_func=w_func))

    def Arg(self, node):
        "NamedArgs define variables and parameters"
        if self.defining_var:
            self.add_symbol(node.loc, LocalVarSymbol(node.name))

        pat = self.proc(node.pat)

        return ast.Arg(node.loc, node.name, pat)

    def Suite(self, node):
        old_symbols = self.symbols
        stmts = self.proc_all(node.stmts)
        self.symbols = old_symbols
        return ast.StmtSuite(node.loc, stmts)

    def StmtDeclVar(self, node):
        defining_var = self.defining_var
        try:
            self.defining_var = True
            return self.default(node)
        finally:
            self.defining_var = defining_var

    def For(self, node):
        NotImplemented

    def If(self, node):
        all_ifs = [(node.cond, node.then)] + node.elsifs

        if node.els:
            els = self.proc(node.els)
        else:
            els = ast.SuiteStatement()

        for cond, then in reversed(all_ifs):
            res = ast.IfStmt(cond.loc,
                             self.proc(cond),
                             self.proc(then),
                             els)
            els = res

        return res

    def ExprDict(self, node):
        keys = self.proc_all([a.key for a in node.args])
        vals = self.proc_all([a.value for a in node.args])
        return ast.DictExpr(node.loc, keys, vals)

    def ExprId(self, node):
        # First check the local scopes.  If we find it in there,
        # it's a local variable.
        if is_simple_id(node.id):
            var_nm = node.id.id
            res = self.lookup(var_nm)
            if res: return res.as_expr(node.loc)

        # Otherwise, it's an absolute reference.  Resolve it against
        # the globals.  It should be some sort of value (a function or
        # unit, basically).
        w_val = self.parent.resolve(node.id).as_w_value(node.loc)
        return ast.ExprConstant(node.loc, w_val)

    def ExprIndex(self, node):
        obj = self.proc(node.obj)
        args = self.proc_all(node.args)
        func = ast.ExprConstant(node.loc, stdlib.w_index)
        return ast.ExprApply.new(
            loc=node.loc, func=func, args=[obj]+args)

    def ExprUnitEq(self, node):
        # note: when we proc a unit, we get a list
        num = self.proc_all(node.num)
        denom = self.proc_all(node.denom)

        # flatten the lists we got
        num_lst = reduce(lambda x, y: x+y, num, [])
        denom_lst = reduce(lambda x, y: x+y, denom, [])

        # construct final ExprUnitEq
        return ast.ExprUnitEq(node.loc, num_lst, denom_lst)

    def UnitRaised(self, node):
        # note: returns a list of unit ast nodes
        n = as_const(node.n)
        u = self.proc(node.unit)
        assert isinstance(n, list)
        return u * n # repeat the list n times

    def UnitRaisedInf(self, node):
        # note: returns a list of unit ast nodes
        return [ast.UnitRaisedInf(node.loc, self.proc(node.unit))]

    def UnitId(self, node):
        # note: returns a list of unit ast nodes
        if is_simple_id(node.id):
            var_nm = node.id.id
            res = self.lookup(var_nm)
            if res: return [res.as_unit_var(node.loc)]

        w_unit = self.parent.resolve(node.id).as_unit(node.id.loc)
        return [ast.UnitFixed(node.loc, w_unit)]          

    def ConstantVal(self, node):
        value = as_w_value(node)
        return ast.ExprConstant(node.loc, value)

    PosInt = ConstantVal
    Float = ConstantVal
    String = ConstantVal

    def PatternFunc(self, node):
        func = self.proc(node.func)
        func.in_pattern()
        return func

    def PatternExpectedValue(self, node):
        value = as_w_value(node.value)
        return ast.PatternExpectedValue(node.loc, value)

    def PatternDataType(self, node):
        try:
            exp_class = value.class_names[node.id]
            return ast.PatternDataType(node.loc, exp_class)
        except KeyError:
            raise error.UnknownType(node.loc, node.id)

    def default(self, node):
        cls = getattr(ast, node.type)
        assert issubclass(cls, ast.Ast)
        return cls.from_parse_node(self.proc, node)

def process_func_item(interp, func_item):
    w_func_value = func_item.as_w_func(DUMMY_LOC)
    # Process from highest priority to lowest:
    for func_def in sorted(func_item.definitions, reverse=True):
        func_node = func_def.parse_tree
        func_res = FuncResolver(AssocList.EMPTY, func_def, func_item)
        func_ast = func_res.proc(func_node)
        func = value.AstFunction(func_ast)
        w_func_value.add_func(func)
        
        if interp.options.dump_ast:
            func_ast.dump(sys.stderr)
