//
// Copyright (c) 2006, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   2 Dec 05  Brian Frank  Creation
//

**
** Walk the AST to resolve:
**   - Manage local variable scope
**   - Resolve loop for breaks and continues
**   - Resolve LocalDefStmt.init into full assignment expression
**   - Resolve Expr.ctype
**   - Resolve UknownVarExpr -> LocalVarExpr, FieldExpr, or CallExpr
**   - Resolve CallExpr to their CMethod
**
class ResolveExpr : CompilerStep
{

//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////

  new make(Compiler compiler)
    : super(compiler)
  {
  }

//////////////////////////////////////////////////////////////////////////
// Run
//////////////////////////////////////////////////////////////////////////

  override Void run()
  {
    log.debug("ResolveExpr")
    walk(compiler, VisitDepth.expr)
    bombIfErr
  }

//////////////////////////////////////////////////////////////////////////
// Method
//////////////////////////////////////////////////////////////////////////

  override Void enterMethodDef(MethodDef m)
  {
    super.enterMethodDef(m)
    this.inClosure = (curType.isClosure && curType.closure.doCall === m)
    initMethodVars
  }

//////////////////////////////////////////////////////////////////////////
// Stmt
//////////////////////////////////////////////////////////////////////////

  override Void enterStmt(Stmt stmt) { stmtStack.push(stmt) }

  override Stmt[]? visitStmt(Stmt stmt)
  {
    stmtStack.pop
    switch (stmt.id)
    {
      case StmtId.expr:         resolveExprStmt((ExprStmt)stmt)
      case StmtId.forStmt:      resolveFor((ForStmt)stmt)
      case StmtId.breakStmt:    resolveBreak((BreakStmt)stmt)
      case StmtId.continueStmt: resolveContinue((ContinueStmt)stmt)
      case StmtId.localDef:     resolveLocalVarDef((LocalDefStmt)stmt)
    }
    return null
  }

  private Void resolveExprStmt(ExprStmt stmt)
  {
    // stand alone expr statements, shouldn't be left on the stack
    stmt.expr = stmt.expr.noLeave
  }

  private Void resolveLocalVarDef(LocalDefStmt def)
  {
    // check for type inference
    if (def.ctype == null)
      def.ctype = def.init.ctype.inferredAs

    // bind to scope as a method variable
    bindToMethodVar(def)

    // if init is null, then we default the variable to null (Fan
    // doesn't do true definite assignment checking since most local
    // variables use type inference anyhow)
    if (def.init == null && !def.isCatchVar)
      def.init = LiteralExpr.makeDefaultLiteral(def.loc, ns, def.ctype)

    // turn init into full assignment
    if (def.init != null)
      def.init = BinaryExpr.makeAssign(LocalVarExpr(def.loc, def.var), def.init)
  }

  private Void resolveFor(ForStmt stmt)
  {
    // don't leave update expression on the stack
    if (stmt.update != null) stmt.update = stmt.update.noLeave
  }

  private Void resolveBreak(BreakStmt stmt)
  {
    // find which loop we're inside of (checked in CheckErrors)
    stmt.loop = findLoop
  }

  private Void resolveContinue(ContinueStmt stmt)
  {
    // find which loop we're inside of (checked in CheckErrors)
    stmt.loop = findLoop
  }

//////////////////////////////////////////////////////////////////////////
// Expr
//////////////////////////////////////////////////////////////////////////

  override Expr visitExpr(Expr expr)
  {
    // resolve the expression
    expr = resolveExpr(expr)

    // expr type must be resolved at this point
    if ((Obj?)expr.ctype == null)
      throw err("Expr type not resolved: ${expr.id}: ${expr}", expr.loc)

    // if we resolved to a generic parameter like V or K,
    // then use its real underlying type
    if (expr.ctype.isGenericParameter)
      expr.ctype = expr.ctype.raw

    // if this expression performs assignment against a local
    // variable, then note the reassignment so that we know it
    // is not a final variable (final being like Java semanatics)
    assignTarget := expr.assignTarget as LocalVarExpr
    if (assignTarget != null && assignTarget.var != null)
      assignTarget.var.reassigned

    return expr
  }

  private Expr resolveExpr(Expr expr)
  {
    switch (expr.id)
    {
      case ExprId.localeLiteral:   return resolveLocaleLiteral(expr)
      case ExprId.slotLiteral:     return resolveSlotLiteral(expr)
      case ExprId.listLiteral:     return resolveList(expr)
      case ExprId.mapLiteral:      return resolveMap(expr)
      case ExprId.boolNot:
      case ExprId.cmpNull:
      case ExprId.cmpNotNull:      expr.ctype = ns.boolType
      case ExprId.assign:          return resolveAssign(expr)
      case ExprId.elvis:           resolveElvis(expr)
      case ExprId.same:
      case ExprId.notSame:
      case ExprId.boolOr:
      case ExprId.boolAnd:
      case ExprId.isExpr:          expr.ctype = ns.boolType
      case ExprId.isnotExpr:       expr.ctype = ns.boolType
      case ExprId.asExpr:          expr.ctype = ((TypeCheckExpr)expr).check.toNullable
      case ExprId.call:            return resolveCall(expr)
      case ExprId.construction:    return resolveConstruction(expr)
      case ExprId.shortcut:        return resolveShortcut(expr)
      case ExprId.thisExpr:        return resolveThis(expr)
      case ExprId.superExpr:       return resolveSuper(expr)
      case ExprId.itExpr:          return resolveIt(expr)
      case ExprId.unknownVar:      return resolveVar(expr)
      case ExprId.storage:         return resolveStorage(expr)
      case ExprId.coerce:          expr.ctype = ((TypeCheckExpr)expr).check
      case ExprId.ternary:         resolveTernary(expr)
      case ExprId.closure:         resolveClosure(expr)
      case ExprId.dsl:             return resolveDsl(expr)
      case ExprId.throwExpr:       expr.ctype = ns.nothingType
    }

    return expr
  }

  **
  ** Resolve locale literal '$<pod::key=def>'
  **
  private Expr resolveLocaleLiteral(LocaleLiteralExpr expr)
  {
    loc := expr.loc

    // cannot define def with explicit podName
    if (expr.podName != null && expr.def != null)
      err("Locale literal cannot specify both qualified pod and default value", loc)

    // cannot specify using current pod if output is not pod
    outputMode := compiler.input.output
    if (expr.podName == null && outputMode != CompilerOutputMode.podFile)
      err("Scripts cannot define non-qualified locale literals", loc)

    // if we have a def, then add to compiler to merge into locale/en.props
    if (expr.def != null) compiler.localeDefs.add(expr)

    // Pod.find(podName) or inType#.pod
    inType := this.curType
    if (inType.isClosure) inType = inType.closure.enclosingType
    podTarget := expr.podName != null ?
      CallExpr.makeWithMethod(loc, null, ns.podFind, [LiteralExpr.makeStr(loc, ns, expr.podName)]) :
      CallExpr.makeWithMethod(loc, LiteralExpr(loc, ExprId.typeLiteral, ns.typeType, inType), ns.typePod)

    // podTarget.locale(key [, def])
    args := [LiteralExpr.makeStr(loc, ns, expr.key)]
    if (expr.def != null) args.add(LiteralExpr.makeStr(loc, ns, expr.def))
    return CallExpr.makeWithMethod(loc, podTarget, ns.podLocale, args)
  }

  **
  ** Resolve slot literal
  **
  private Expr resolveSlotLiteral(SlotLiteralExpr expr)
  {
    slot := expr.parent.slot(expr.name)
    if (slot == null)
    {
      err("Unknown slot literal '${expr.parent.signature}.${expr.name}'", expr.loc)
      expr.ctype = ns.error
      return expr
    }
    expr.ctype = slot is CField ? ns.fieldType : ns.methodType
    expr.slot = slot
    return expr
  }

  **
  ** Resolve list literal
  **
  private Expr resolveList(ListLiteralExpr expr)
  {
    if (expr.explicitType != null)
    {
      expr.ctype = expr.explicitType
    }
    else
    {
      // infer from list item expressions
      v := Expr.commonType(ns, expr.vals)
      expr.ctype = v.toListOf
    }
    return expr
  }

  **
  ** Resolve map literal
  **
  private Expr resolveMap(MapLiteralExpr expr)
  {
    if (expr.explicitType != null)
    {
      expr.ctype = expr.explicitType
    }
    else
    {
      // infer from key/val expressions
      k := Expr.commonType(ns, expr.keys).toNonNullable
      v := Expr.commonType(ns, expr.vals)
      expr.ctype = MapType(k, v)
    }
    return expr
  }

  **
  ** Resolve this keyword expression
  **
  private Expr resolveThis(ThisExpr expr)
  {
    if (inClosure)
    {
      loc := expr.loc
      closure := curType.closure

      // if the closure is in a static slot, report an error
      if (closure.enclosingSlot.isStatic)
      {
        expr.ctype = ns.error
        err("Cannot access 'this' within closure of static context", loc)
        return expr
      }

      // otherwise replace this with $this field access
      return FieldExpr(loc, ThisExpr(loc), closure.outerThisField)
    }

    expr.ctype = curType
    return expr
  }

  **
  ** Resolve super keyword expression
  **
  private Expr resolveSuper(SuperExpr expr)
  {
    if (inClosure)
    {
      // it would be nice to support super from within a closure,
      // but the Java VM has the stupid restriction that invokespecial
      // cannot be used outside of the class - we could potentially
      // work around this using a wrapper method - but for now we will
      // just disallow it
      err("Invalid use of 'super' within closure", expr.loc)
      expr.ctype = ns.error
      return expr
    }

    if (expr.explicitType != null)
      expr.ctype = expr.explicitType
    else
      expr.ctype = curType.base

    return expr
  }

  **
  ** Resolve it keyword expression
  **
  private Expr resolveIt(ItExpr expr)
  {
    // if inside of field setter it is our implicit val parameter
    if (curMethod != null && curMethod.isFieldSetter)
      return LocalVarExpr(expr.loc, curMethod.vars.first)

    // can't use it keyword outside of an it-block
    if (!inClosure || !curType.closure.isItBlock)
    {
      err("Invalid use of 'it' outside of it-block", expr.loc)
      expr.ctype = ns.error
      return expr
    }

    // closure's itType should be defined at this point
    expr.ctype = curType.closure.itType
    return expr
  }

  **
  ** Resolve an assignment operation
  **
  private Expr resolveAssign(BinaryExpr expr)
  {
    // if lhs has synthetic coercion we need to remove it;
    // this can occur when resolving a FFI field - in order
    // for this to work there are only two possible allowed
    // coercions: 1) a TypeCheckExpr or 2) a CallExpr where
    // the non-coerced expression is the last argument
    if (expr.lhs.synthetic)
    {
      if (expr.lhs.id === ExprId.coerce)
        expr.lhs = ((TypeCheckExpr)expr.lhs).target
      else if (expr.lhs.id === ExprId.call)
        expr.lhs = ((CallExpr)expr.lhs).args.last
      else
        throw Err("Unexpected LHS synthetic expr: $expr [$expr.loc.toLocStr]")
    }

    // check for left hand side the [] shortcut, because []= is set
    shortcut := expr.lhs as ShortcutExpr
    if (shortcut != null && shortcut.op == ShortcutOp.get)
    {
      shortcut.op = ShortcutOp.set
      shortcut.name = "set"
      shortcut.args.add(expr.rhs)
      shortcut.method = null
      return resolveCall(shortcut)
    }

    // check for left hand side the -> shortcut, because a->x=b is trap.a("x", [b])
    call := expr.lhs as CallExpr
    if (call != null && call.isDynamic)
    {
      call.args.add(expr.rhs)
      return resolveCall(call)
    }

    // assignment is typed by lhs
    expr.ctype = expr.lhs.ctype

    return expr
  }

  **
  ** Resolve an UnknownVar to its replacement node.
  **
  private Expr resolveVar(UnknownVarExpr var)
  {
    // if there is no target, attempt to bind to local variable
    if (var.target == null)
    {
      // attempt to a name in the current scope
      binding := resolveLocal(var.name, var.loc)
      if (binding != null)
        return LocalVarExpr(var.loc, binding)
    }

    // at this point it can't be a local variable, so it must be
    // a slot on either myself or the variable's target
    return CallResolver(compiler, curType, curMethod, var).resolve
  }

  **
  ** Resolve storage operator
  **
  private Expr resolveStorage(UnknownVarExpr var)
  {
    // resolve as normal unknown variable
    resolved := resolveVar(var)

    // handle case where we have a local variable hiding a
    // field since the *x is assumed to be this.*x
    if (resolved.id === ExprId.localVar)
    {
      field := curType.field(var.name)
      if (field != null)
        resolved = FieldExpr(var.loc, ThisExpr(var.loc), field)
    }

    // is we can't resolve as field, then this is an error
    if (resolved.id !== ExprId.field)
    {
      if (resolved.ctype !== ns.error)
        err("Invalid use of field storage operator '&'", var.loc)
      return resolved
    }

    f := resolved as FieldExpr
    f.useAccessor = false
    if (f.field is FieldDef)
    {
      fd := (FieldDef)f.field
      fd.flags = fd.flags.or(FConst.Storage)
    }
    return f
  }

  **
  ** Resolve "x ?: y" expression
  **
  private Expr resolveElvis(BinaryExpr expr)
  {
    expr.ctype = CType.common(ns, [expr.lhs.ctype, expr.rhs.ctype]).toNullable
    return expr
  }

  **
  ** Resolve "x ? y : z" ternary expression
  **
  private Expr resolveTernary(TernaryExpr expr)
  {
    if (expr.trueExpr.id === ExprId.nullLiteral)
      expr.ctype = expr.falseExpr.ctype.toNullable
    else if (expr.falseExpr.id === ExprId.nullLiteral)
      expr.ctype = expr.trueExpr.ctype.toNullable
    else
      expr.ctype = CType.common(ns, [expr.trueExpr.ctype, expr.falseExpr.ctype])
    return expr
  }

  **
  ** Resolve a call to it's Method and return type.
  **
  private Expr resolveCall(CallExpr call)
  {
    // dynamic calls are just syntactic sugar for Obj.trap
    if (call.isDynamic) return resolveTrapCall(call)

    // if this is a constructor chained call to a FFI
    // super-class then route to the FFI bridge to let it handle
    if (call.isCtorChain && curType.base.isForeign)
      return curType.base.bridge.resolveConstructorChain(call)

    // if there is no target, attempt to bind to local variable
    if (call.target == null)
    {
      // attempt to a name in the current scope
      binding := resolveLocal(call.name, call.loc)
      if (binding != null)
        return resolveCallOnLocalVar(call, LocalVarExpr(call.loc, binding))
    }

    return CallResolver(compiler, curType, curMethod, call).resolve
  }

  **
  ** Resolve dynamic call to trap() method
  **
  private Expr resolveTrapCall(CallExpr call)
  {
    // resolve to Obj.trap of its override
    call.method = call.target.ctype.method("trap")
    call.ctype = call.method.returnType

    // if subclass has covariant return type, then insert cast
    if (call.ctype.isObj)
      return call
    else
      return TypeCheckExpr.coerce(call, call.ctype) { from = ns.objType.toNullable }
  }

  **
  ** Resolve the () operator on a local variable - if the local
  ** is a Method, then () is syntactic sugar for Method.callx()
  **
  private Expr resolveCallOnLocalVar(CallExpr call, LocalVarExpr binding)
  {
    // if the call was generated as an it-block on local
    if (call.noParens && call.args.size == 1)
    {
      closure:= call.args.last as ClosureExpr
      if (closure != null && closure.isItBlock)
        return closure.toWith(binding)
    }

    // can only handle zero to eight arguments; I could wrap up the
    // arguments into a List and use call(List) - but methods with
    // that many arguments are just inane so tough luck
    if (call.args.size > 8)
    {
      err("Tough luck - cannot use () operator with more than 8 arguments, use call(List)", call.loc)
      call.ctype = ns.error
      return call
    }

    // invoking the () operator on a sys::Func is syntactic
    // sugar for invoking one of the Func.call methods
    callMethod := binding.ctype.method("call")
    if (callMethod == null)
    {
      if (binding.ctype != ns.error)
        err("Cannot use () call operator on non-func type '$binding.ctype'", call.loc)
      call.ctype = ns.error
      return call
    }
    call = CallExpr.makeWithMethod(call.loc, binding, callMethod, call.args)
    call.isCallOp = true
    return call
  }

  **
  ** Resolve a construction call Type(args)
  **
  private Expr resolveConstruction(CallExpr call)
  {
    base := call.target.ctype
    call.ctype = base  // fallback in case of errors

    // route FFI constructors to bridge
    if (base.isForeign) return base.bridge.resolveConstruction(call)

    // get all constructors that might match this call
    matches := Str:CMethod[:]
    findCtorMatches(matches, base, call.args)

    // check if our last argument is an it-block, then check
    // for constructors without that arg:
    //    make(...).with(lastArg)
    itBlock := (call.args.last as ClosureExpr)?.isItBlock ?: false
    if (itBlock) findCtorMatches(matches, base, call.args[0..-2])

    // if no matches bail
    if (matches.isEmpty)
    {
      args := call.args.join(", ") |arg| { arg.ctype.toStr }
      err("No constructor found: ${base.name}($args)", call.loc)
      return call
    }

    // if we have multiple matches, we have ambiguous constructor
    if (matches.size > 1)
    {
      args := call.args.join(", ") |arg| { arg.ctype.toStr }
      names := matches.map |m| { m.name }.vals.sort.join(", ")
      err("Ambiguous constructor: ${base.name}($args) [$names]", call.loc)
      return call
    }

    // we have our resolved match
    match := matches.vals.first
    call.method = match
    call.ctype = match.isStatic ? match.returnType : base

    // hook to infer closure type from call or to
    // translateinto an implicit call to Obj.with
    return CallResolver.inferClosureTypeFromCall(this, call, base)
  }

  **
  ** Walk all the slots in 'base' and match any constructor
  ** that could be called using the given arguments.
  **
  private Void findCtorMatches(Str:CMethod matches, CType base, Expr[] args)
  {
    base.slots.each |slot|
    {
      // if not a visibile constructor, then not a match
      if (!isCtorMethod(slot)) return
      if (!CheckErrors.isSlotVisible(curType, slot)) return

      // don't match any inherited methods
      if (slot.parent != base) return

      // check argument/parameter counts to see if we can disqualify it
      ctor := (CMethod)slot
      params := ctor.params
      if (params.size < args.size) return
      if (params.size > args.size && !params[args.size].hasDefault) return

      // check that each parameter fits
      for (i:=0; i<args.size; ++i)
        if (!CheckErrors.canCoerce(args[i], params[i].paramType))
          return

      // its a match!
      matches[ctor.name] = ctor
    }
  }

  private Bool isCtorMethod(CSlot slot)
  {
    if (slot.isCtor) return true
    if (slot isnot CMethod) return false
    // TODO let static "make" or "fromStr" pass
    if (slot.isStatic && (slot.name == "make" || slot.name == "fromStr")) return true
    return false
  }

  **
  ** Resolve ShortcutExpr.
  **
  private Expr resolveShortcut(ShortcutExpr expr)
  {
    // if this is an indexed assigment such as x[y] += z
    if (expr.isAssign && expr.target.id === ExprId.shortcut)
      return resolveIndexedAssign(expr)

    // string concat is always optimized, and performs a bit
    // different since a non-string can be used as the lhs
    if (expr.isStrConcat)
    {
      expr.ctype  = ns.strType
      expr.method = ns.strPlus
      return ConstantFolder(compiler).fold(expr)
    }

    // if a binary operation
    if (expr.args.size == 1 && expr.op.isOperator)
    {
      method := resolveBinaryOperator(expr)
      if (method == null) { expr.ctype = ns.error; return expr }
      expr.method = method
      expr.name   = method.name
    }

    // resolve the call, if optimized, then return it immediately
    result := resolveCall(expr)
    if (result !== expr) return result

    // check that method has Operator facet
    if (expr.method != null && expr.op.isOperator && !expr.method.hasFacet("sys::Operator"))
      err("Missing Operator facet: $expr.method.qname", expr.loc)

    // the comparision operations are special case that call a method
    // that return an Int, but leave a Bool on the stack (we also handle
    // specially in assembler)
    switch (expr.opToken)
    {
      case Token.lt:
      case Token.ltEq:
      case Token.gt:
      case Token.gtEq:
        expr.ctype = ns.boolType
    }

    return expr
  }

  **
  ** Given a shortcut method such as 'lhs op rhs' figure
  ** out which method to use for the operator symbol.
  **
  private CMethod? resolveBinaryOperator(ShortcutExpr expr)
  {
    op := expr.op
    lhs := expr.target.ctype
    rhs := expr.args.first

    if (lhs === ns.error || rhs.ctype === ns.error) return null

    // get matching operators for the method name
    matches := lhs.operators.find(op.methodName)

    // if multiple matches, attempt to narrow by argument type
    if (matches.size > 1)
    {
      matches = matches.findAll |m|
      {
        if (m.params.size != 1) return false
        paramType := m.params.first.paramType
        return CheckErrors.canCoerce(rhs, paramType)
      }
    }

    // if no matches bail
    if (matches.isEmpty)
    {
      err("No operator method found: ${op.formatErr(lhs, rhs.ctype)}", expr.loc)
      return null
    }

    // if we have one match, we are golden
    if (matches.size == 1) return matches.first

    // still have an ambiguous operator method call
    names := (matches.map |CMethod m->Str| { m.name }).join(", ")
    err("Ambiguous operator method: ${op.formatErr(lhs, rhs.ctype)} [$names]", expr.loc)
    return null
  }

  **
  ** If we have an assignment against an indexed shortcut
  ** such as x[y] += z, then process specially to return
  ** a IndexedAssignExpr subclass of ShortcutExpr.
  **
  private Expr resolveIndexedAssign(ShortcutExpr orig)
  {
    // if target is in error, don't bother
    if (orig.target.ctype === ns.error)
    {
      orig.ctype = ns.error
      return orig
    }

    // we better have a x[y] indexed get expression
    if (orig.target.id != ExprId.shortcut && orig.target->op === ShortcutOp.get)
    {
      err("Expected indexed expression", orig.loc)
      return orig
    }

    // wrap the shorcut as an IndexedAssignExpr
    expr := IndexedAssignExpr.makeFrom(orig)

    // resolve it normally - if the orig is "x[y] += z" then we
    // are resolving Int.plus here - the target is "x[y]" and should
    // already be resolved
    resolveCall(expr)

    // resolve the set method which matches
    // the get method on the target
    get := ((ShortcutExpr)expr.target).method
    set := get.parent.method("set")
    if (set == null || set.params.size != 2 || set.isStatic ||
        set.params[0].paramType.toNonNullable != get.params[0].paramType.toNonNullable ||
        set.params[1].paramType.toNonNullable != get.returnType.toNonNullable)
      err("No matching 'set' method for '$get.qname'", orig.loc)
    else
      expr.setMethod = set

    // return the new IndexedAssignExpr
    return expr
  }

  **
  ** ClosureExpr will just output its substitute expression.  But we take
  ** this opportunity to capture the local variables in the closure's scope
  ** and cache them on the ClosureExpr.  We also do variable name checking.
  **
  private Void resolveClosure(ClosureExpr expr)
  {
    // save away current locals in scope
    expr.enclosingVars = localsInScope

    // make sure none of the closure's parameters
    // conflict with the locals in scope
    expr.doCall.paramDefs.each |ParamDef p|
    {
      if (expr.enclosingVars.containsKey(p.name) && p.name != "it")
        err("Closure parameter '$p.name' is already defined in current block", p.loc)
    }
  }

  **
  ** Resolve a DSL
  **
  private Expr resolveDsl(DslExpr expr)
  {
    plugin := DslPlugin.find(this, expr.loc, expr.anchorType)
    if (plugin == null)
    {
      expr.ctype = ns.error
      return expr
    }

    origNumErrs := compiler.errs.size
    expr.ctype = ns.error
    try
    {
      result := plugin.compile(expr)
      if (result === expr) return result
      return result.walk(this)
    }
    catch (CompilerErr e)
    {
      if (compiler.errs.size == origNumErrs) errReport(e)
      return expr
    }
    catch (Err e)
    {
      errReport(CompilerErr("Internal error in DslPlugin '$plugin.typeof': $e", expr.loc, e))
      e.trace
      return expr
    }
  }

//////////////////////////////////////////////////////////////////////////
// Scope
//////////////////////////////////////////////////////////////////////////

  **
  ** Setup the MethodVars for the parameters.
  **
  private Void initMethodVars()
  {
    m := curMethod
    reg := m.isStatic ?  0 : 1

    m.paramDefs.each |ParamDef p|
    {
      var := MethodVar.makeForParam(m, reg++, p, p.paramType.parameterizeThis(curType))
      m.vars.add(var)
    }
  }

  **
  ** Bind the specified local variable definition to a
  ** MethodVar (and register number).
  **
  private Void bindToMethodVar(LocalDefStmt def)
  {
    // make sure it doesn't exist in the current scope
    if (resolveLocal(def.name, def.loc) != null)
      err("Variable '$def.name' is already defined in current block", def.loc)

    // create and add it
    def.var = curMethod.addLocalVarForDef(def, currentBlock)
  }

  **
  ** Resolve a local variable using current scope based on
  ** the block stack and possibly the scope of a closure.
  **
  private MethodVar? resolveLocal(Str name, Loc loc)
  {
    // if not in method, then we can't have a local
    if (curMethod == null) return null

    // attempt to bind a name in the current scope
    binding := curMethod.vars.find |MethodVar var->Bool|
    {
      return var.name == name && isBlockInScope(var.scope)
    }
    if (binding != null) return binding

    // if a closure, check parent scope
    if (inClosure)
    {
      closure := curType.closure
      binding = closure.enclosingVars[name]
      if (binding != null)
      {
        // mark the enclosing method and var as being used in a closure
        binding.method.usesCvars = true
        binding.usedInClosure = true

        // create new "shadow" local var in closure body which
        // shadows the enclosed variable from parent scope,
        // we'll do further processing in ClosureVars
        shadow := curMethod.addLocalVar(binding.ctype, binding.name, currentBlock)
        shadow.usedInClosure = true
        shadow.shadows = binding

        // if there are intervening closure scopes between
        // the original scope and current scope, then we need to
        // add a pass-thru variable in each scope
        last := shadow
        for (p := closure.enclosingClosure; p != null; p = p.enclosingClosure)
        {
          if (binding.method === p.doCall) break
          passThru := p.doCall.addLocalVar(binding.ctype, binding.name, p.doCall.code)
          passThru.usedInClosure = true
          passThru.shadows = binding
          passThru.usedInClosure = true
          last.shadows = passThru
          last = passThru
        }

        return shadow
      }
    }

    // not found
    return null
  }

  **
  ** Get a list of all the local method variables that
  ** are currently in scope.
  **
  private Str:MethodVar localsInScope()
  {
    Str:MethodVar acc := inClosure ?
      curType.closure.enclosingVars.dup :
      Str:MethodVar[:]

    if (curMethod == null) return acc

    curMethod.vars.each |MethodVar var|
    {
      if (isBlockInScope(var.scope))
        acc[var.name] = var
    }

    return acc
  }

  **
  ** Get the current block which defines our scope.  We make
  ** a special case for "for" loops which can declare variables.
  **
  private Block currentBlock()
  {
    if (stmtStack.peek is ForStmt)
      return ((ForStmt)stmtStack.peek).block
    else
      return blockStack.peek
  }

  **
  ** Check if the specified block is currently in scope.  We make
  ** a specialcase for "for" loops which can declare variables.
  **
  private Bool isBlockInScope(Block? block)
  {
    // the null block within the whole method (ctorChains or defaultParams)
    if (block == null) return true

    // special case for "for" loops
    if (stmtStack.peek is ForStmt)
    {
      if (((ForStmt)stmtStack.peek).block === block)
        return true
    }

    // look in block stack which models scope chain
    return blockStack.any |Block b->Bool| { b === block }
  }

//////////////////////////////////////////////////////////////////////////
// StmtStack
//////////////////////////////////////////////////////////////////////////

  private Stmt? findLoop()
  {
    for (i:=stmtStack.size-1; i>=0; --i)
    {
      stmt := stmtStack[i]
      if (stmt.id === StmtId.whileStmt) return stmt
      if (stmt.id === StmtId.forStmt)   return stmt
    }
    return null
  }

//////////////////////////////////////////////////////////////////////////
// BlockStack
//////////////////////////////////////////////////////////////////////////

  override Void enterBlock(Block block) { blockStack.push(block) }
  override Void exitBlock(Block block)  { blockStack.pop }

//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////

  Stmt[] stmtStack  := Stmt[,]    // statement stack
  Block[] blockStack := Block[,]  // block stack used for scoping
  Bool inClosure := false         // are we inside a closure's block

}