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

**
** CodeAsm is used to assemble the fcode instructions of an Expr or Block.
**
class CodeAsm : CompilerSupport
{

//////////////////////////////////////////////////////////////////////////
// Construction
//////////////////////////////////////////////////////////////////////////

  new make(Compiler compiler, Loc loc, FPod fpod, MethodDef? curMethod)
    : super(compiler)
  {
    this.loc       = loc
    this.fpod      = fpod
    this.curMethod = curMethod
    this.code      = Buf.make
    this.errTable  = Buf.make; errTable.writeI2(-1)
    this.errCount  = 0
    this.lines     = Buf.make; lines.writeI2(-1)
    this.lineCount = 0
    this.loopStack = Loop[,]
  }

//////////////////////////////////////////////////////////////////////////
// Statements
//////////////////////////////////////////////////////////////////////////

  Void block(Block block)
  {
    block.stmts.each |Stmt s| { stmt(s) }
  }

  Void stmt(Stmt stmt)
  {
    switch (stmt.id)
    {
      case StmtId.nop:           return
      case StmtId.expr:          expr(((ExprStmt)stmt).expr)
      case StmtId.localDef:      localVarDefStmt((LocalDefStmt)stmt)
      case StmtId.ifStmt:        ifStmt((IfStmt)stmt)
      case StmtId.returnStmt:    returnStmt((ReturnStmt)stmt)
      case StmtId.throwStmt:     throwStmt((ThrowStmt)stmt)
      case StmtId.forStmt:       forStmt((ForStmt)stmt)
      case StmtId.whileStmt:     whileStmt((WhileStmt)stmt)
      case StmtId.breakStmt:     breakOrContinueStmt(stmt)
      case StmtId.continueStmt:  breakOrContinueStmt(stmt)
      case StmtId.switchStmt:    switchStmt((SwitchStmt)stmt)
      case StmtId.tryStmt:       tryStmt((TryStmt)stmt)
      default:                   throw Err(stmt.id.toStr)
    }
  }

  private Void ifStmt(IfStmt stmt)
  {
    endLabel := -1
    c := Cond.make

    // optimize: if (true)
    if (stmt.condition.id == ExprId.trueLiteral)
    {
      block(stmt.trueBlock)
      return
    }

    // optimize: if (false)
    if (stmt.condition.id == ExprId.falseLiteral)
    {
      if (stmt.falseBlock != null)
        block(stmt.falseBlock)
      return
    }

    // check condition - if the condition is itself a CondExpr
    // then we just have it branch directly to the true/false
    // block rather than wasting instructions to push true/false
    // onto the stack
    if (stmt.condition is CondExpr)
    {
      cond((CondExpr)stmt.condition, c)
    }
    else
    {
      expr(stmt.condition)
      c.jumpFalses.add(jump(FOp.JumpFalse))
    }

    // true block
    c.jumpTrues.each |Int pos| { backpatch(pos) }
    block(stmt.trueBlock)
    if (!stmt.trueBlock.isExit && stmt.falseBlock != null)
      endLabel = jump(FOp.Jump)

    // false block
    c.jumpFalses.each |Int pos| { backpatch(pos) }
    if (stmt.falseBlock != null)
      block(stmt.falseBlock)

    // end
    if (endLabel != -1) backpatch(endLabel)
  }

  private Void returnStmt(ReturnStmt stmt)
  {
    // if we have a expression
    exprOnStack := false
    if (stmt.expr != null)
    {
      // evaluate expr
      expr(stmt.expr)

      // unless expr was void, we have it on the stack now
      exprOnStack = !stmt.expr.ctype.isVoid

      // if we have expr on stack inside a void method, need to pop it
      inVoidMethod := curMethod != null && curMethod.ret.isVoid
      if (inVoidMethod && exprOnStack)
      {
        opType(FOp.Pop, stmt.expr.ctype)
        exprOnStack = false
      }
    }

    // if we are in a protected region, then we can't return immediately,
    // rather we need to save the result into a temporary local; and use
    // a "leave" instruction which we will backpatch in finishCode() with
    // the actual return sequence;
    if (inProtectedRegion)
    {
      // if returning a result then stash in temp local
      if (exprOnStack)
      {
        returnLocal = stmt.leaveVar
        op(FOp.StoreVar, returnLocal.register)
      }

      // jump to any finally blocks we are inside
      protectedRegions.eachr |ProtectedRegion region|
      {
        if (region.hasFinally)
          region.jumpFinallys.add(jump(FOp.JumpFinally))
      }

      // generate leave instruction and register for backpatch
      if (leavesToReturn == null) leavesToReturn = Int[,]
      leavesToReturn.add(jump(FOp.Leave))
      return
    }

    // process as normal return
    op(FOp.Return)
  }

  private Void throwStmt(ThrowStmt stmt) { throwOp(stmt.exception) }

  private Void throwOp(Expr exception)
  {
    expr(exception)
    op(FOp.Throw)
  }

  private Void localVarDefStmt(LocalDefStmt stmt)
  {
    if (stmt.isCatchVar)
    {
      // "declaration" of a catch variable is used store the
      // variable back to its local register
      var := stmt.var
      op(FOp.CatchErrStart, fpod.addTypeRef(stmt.ctype))

      // if the catch variable has been hoisted onto the heap
      // with a wrapper, call the wrapper constructor
      if (var.isWrapped)
      {
        wrapCtor := fpod.addMethodRef(var.wrapField.parent.method("make"), 1)
        op(FOp.CallNew, wrapCtor)
      }

      // store back to local register
      op(FOp.StoreVar, var.register)
    }
    else if (stmt.init != null)
    {
      expr(stmt.init)
    }
  }

//////////////////////////////////////////////////////////////////////////
// Loops
//////////////////////////////////////////////////////////////////////////

  private Void whileStmt(WhileStmt stmt)
  {
    // push myself onto the loop stack so that breaks
    // and continues can register for backpatching
    loop := Loop(stmt)
    loopStack.push(loop)

    // assemble the while loop code
    continueLabel := mark
    expr(stmt.condition)
    breakJump := jump(FOp.JumpFalse)
    block(stmt.block)
    jump(FOp.Jump, continueLabel)
    breakLabel := mark
    backpatch(breakJump)

    // backpatch continues/breaks
    loop.continues.each |Int pos| { backpatch(pos, continueLabel) }
    loop.breaks.each |Int pos| { backpatch(pos, breakLabel) }

    // pop loop from stack
    loopStack.pop

    // TODO - the fcode will often contain Jumps to Jumps which can be optimized
  }

  private Void forStmt(ForStmt stmt)
  {
    breakJump := -1

    // push myself onto the loop stack so that breaks
    // and continues can register for backpatching
    loop := Loop(stmt)
    loopStack.push(loop)

    // assemble init if available
    if (stmt.init != null) this.stmt(stmt.init)

    // assemble the for loop code
    condLabel := mark
    if (stmt.condition != null)
    {
      expr(stmt.condition)
      breakJump = jump(FOp.JumpFalse)
    }
    block(stmt.block)
    updateLabel := mark
    if (stmt.update != null) expr(stmt.update)
    jump(FOp.Jump, condLabel)
    endLabel := mark
    if (breakJump != -1) backpatch(breakJump, endLabel)

    // backpatch continues/breaks
    loop.continues.each |Int pos| { backpatch(pos, updateLabel) }
    loop.breaks.each |Int pos| { backpatch(pos, endLabel) }

    // pop loop from stack
    loopStack.pop

    // TODO - the fcode will often contain Jumps to Jumps which can be optimized
  }

  private Void breakOrContinueStmt(Stmt stmt)
  {
    // associated loop should be top of stack
    loop := loopStack.peek
    if (loop.stmt !== stmt->loop)
      throw err("Internal compiler error", stmt.loc)

    // if we are inside a protection region which was pushed onto
    // my loop's own stack that means this break or continue
    // needs to jump out of the protected region - that requires
    // calling each region's finally block and using a "leave"
    // instruction rather than a standard "jump"
    Int? toBackpatch := null
    if (!loop.protectedRegions.isEmpty)
    {
      // jump to any finally blocks we are inside
      loop.protectedRegions.eachr |ProtectedRegion region|
      {
        if (region.hasFinally)
          region.jumpFinallys.add(jump(FOp.JumpFinally))
      }

      // generate leave instruction
      toBackpatch = jump(FOp.Leave)
    }
    else
    {
      // generate standard jump instruction
      toBackpatch = jump(FOp.Jump)
    }

    // register for backpatch
    if (stmt.id === StmtId.breakStmt)
      loop.breaks.add(toBackpatch)
    else
      loop.continues.add(toBackpatch)
  }

//////////////////////////////////////////////////////////////////////////
// Switch
//////////////////////////////////////////////////////////////////////////

  private Void switchStmt(SwitchStmt stmt)
  {
    // A table switch is a series of contiguous (or near contiguous)
    // cases which can be represented an offset into a jump table.
    minMax := computeTableRange(stmt)
    if (minMax != null)
      tableSwitchStmt(stmt, minMax[0], minMax[1])
    else
      equalsSwitchStmt(stmt)
  }

  **
  ** Compute the range of this switch and return as a list of '[min, max]'
  ** if the switch is a candidate for a table switch as a series of
  ** contiguous (or near contiguous) cases which can be represented an
  ** offset into a jump table.  Return null if the switch is not numeric
  ** or too sparse to use as a table switch.
  **
  private Int[]? computeTableRange(SwitchStmt stmt)
  {
    // we only compute ranges for Ints and Enums
    ctype := stmt.condition.ctype
    if (!ctype.isInt && !ctype.isEnum)
      return null

    // now we need to determine contiguous range
    min := 2147483647
    max := -2147483648
    count := 0
    try
    {
      stmt.cases.each |Case c|
      {
        for (i:=0; i<c.cases.size; ++i)
        {
          count++
          expr := c.cases[i]
          // TODO: need to handle static const Int fields here
          literal := expr.asTableSwitchCase
          if (literal == null) throw CompilerErr("return null", c.loc)
          if (literal < min) min = literal
          if (literal > max) max = literal
        }
      }
    }
    catch (CompilerErr e)
    {
      return null
    }

    // if no cases, then don't use tableswitch
    if (count == 0) return null

    // enums and anything with less than 32 jumps is immediately
    // allowed, otherwise base the table on a percentage of count
    delta := max - min
    if (ctype.isEnum || delta < 32 || count*32 > delta)
      return [min,max]
    else
      return null
  }

  private Void tableSwitchStmt(SwitchStmt stmt, Int min, Int max)
  {
    stmt.isTableswitch = true
    conditionType := stmt.condition.ctype
    isEnum := conditionType.isEnum

    // push condition onto the stack
    expr(stmt.condition)

    // get a real int onto the stack
    if (conditionType.isInt && conditionType.isNullable)
      coerceOp(conditionType, ns.intType)
    else if (isEnum)
      op(FOp.CallVirtual, fpod.addMethodRef(ns.enumOrdinal))

    // if min is not zero, then do a subtraction so that
    // our condition is a zero based index into the jump table
    if (min != 0)
    {
      op(FOp.LoadInt, fpod.ints.add(-min))
      op(FOp.CallVirtual, fpod.addMethodRef(ns.intPlus))
    }

    // now allocate our jump table
    count := max - min + 1
    jumps := Case?[,]
    jumps.size = count

    // walk thru each case, and map the jump offset to a block
    stmt.cases.each |Case c|
    {
      for (i:=0; i<c.cases.size; ++i)
      {
        expr    := c.cases[i]
        literal := expr.asTableSwitchCase
        offset  := literal - min
        jumps[offset] = c
      }
    }

    // now write the switch bytecodes
    op(FOp.Switch)
    code.writeI2(count)
    jumpStart := code.size
    fill := count*2
    fill.times |->| { code.write(0xff) }  // we'll backpatch the jump offsets last

    // default block goes first - it's the switch fall
    // thru, save offset to back patch jump
    defaultStart := mark
    defaultEnd := switchBlock(stmt.defaultBlock)

    // now write each case block
    caseEnds := Int?[,]
    caseEnds.size = stmt.cases.size
    stmt.cases.each |Case c, Int i|
    {
      c.startOffset = code.size
      caseEnds[i] = switchBlock(c.block)
    }

    // backpatch the jump table
    end := code.size
    code.seek(jumpStart)
    jumps.each |Case? c, Int i|
    {
      if (c == null)
        code.writeI2(defaultStart)
      else
        code.writeI2(c.startOffset)
    }
    code.seek(end)

    // backpatch all the case blocks to jump here when done
    if (defaultEnd != -1) backpatch(defaultEnd)
    caseEnds.each |Int pos|
    {
      if (pos != -1) backpatch(pos)
    }
  }

  private Void equalsSwitchStmt(SwitchStmt stmt)
  {
    stmt.isTableswitch = false

    // push condition onto the stack
    condition := stmt.condition
    expr(condition)

    // walk thru each case, keeping track of all the
    // places we need to backpatch when cases match
    jumpPositions := Int[,]
    jumpCases := Case[,]
    stmt.cases.each |Case c|
    {
      for (i:=0; i<c.cases.size; ++i)
      {
        opType(FOp.Dup, condition.ctype)
        compareOp(stmt.condition.ctype, FOp.CmpEQ, c.cases[i])
        jumpPositions.add(jump(FOp.JumpTrue))
        jumpCases.add(c)
      }
    }

    // default block goes first - it's the switch fall
    // thru, save offset to back patch jump
    defaultStart := mark
    defaultEnd := switchBlock(stmt.defaultBlock, condition.ctype)

    // now write each case block
    caseEnds := Int?[,]
    caseEnds.size = stmt.cases.size
    stmt.cases.each |Case c, Int i|
    {
      c.startOffset = code.size
      caseEnds[i] = switchBlock(c.block, condition.ctype)
    }

    // backpatch the jump table
    end := code.size
    jumpPositions.each |Int pos, Int i|
    {
      backpatch(pos, jumpCases[i].startOffset)
    }
    code.seek(end)

    // backpatch all the case blocks to jump here when done
    if (defaultEnd != -1) backpatch(defaultEnd)
    caseEnds.each |Int pos|
    {
      if (pos != -1) backpatch(pos)
    }
  }

  private Int switchBlock(Block? block, CType? popType := null)
  {
    if (popType != null) opType(FOp.Pop, popType)
    if (block != null)
    {
      this.block(block)
      if (block.isExit) return -1
    }
    return jump(FOp.Jump)
  }

//////////////////////////////////////////////////////////////////////////
// Try
//////////////////////////////////////////////////////////////////////////

  private Bool inProtectedRegion()
  {
    return protectedRegions != null && !protectedRegions.isEmpty
  }

  private Void tryStmt(TryStmt stmt)
  {
    // enter a "protected region" which means that we can't
    // jump or return out of this region directly - we have to
    // use a special "leave" jump of the protected region
    if (protectedRegions == null) protectedRegions = ProtectedRegion[,]
    region := ProtectedRegion(stmt)
    protectedRegions.push(region)
    if (!loopStack.isEmpty) loopStack.peek.protectedRegions.push(region)

    // assemble body of try block
    start := mark
    block(stmt.block)
    end := mark

    // if the block isn't guaranteed to exit:
    //  1) if we have a finally, then jump to finally
    //  2) jump over catch blocks
    tryDone := -1
    finallyStart := -1
    if (!stmt.block.isExit)
    {
      if (region.hasFinally)
      {
        region.jumpFinallys.add(jump(FOp.JumpFinally))
        end = mark
      }
      tryDone = jump(FOp.Leave)
    }

    // assemble catch blocks
    catchDones := Int?[,]
    catchDones.size = stmt.catches.size
    stmt.catches.each |Catch c, Int i|
    {
      catchDones[i] = tryCatch(c, start, end, region)
    }

    // assemble finally block
    if (region.hasFinally)
    {
      // wrap try block and each catch block with catch all to finally
      addToErrTable(start, end, mark, null)
      stmt.catches.each |Catch c|
      {
        addToErrTable(c.start, c.end, mark, null)
      }

      // handler code
      region.jumpFinallys.each |Int pos| { backpatch(pos) }
      op(FOp.FinallyStart)
      block(stmt.finallyBlock)
      op(FOp.FinallyEnd)
    }

    // mark next statement as jump destination for try block
    if (tryDone != -1) backpatch(tryDone)
    catchDones.each |Int pos| { if (pos != -1) backpatch(pos) }

    // leave protected region
    if (!loopStack.isEmpty) loopStack.peek.protectedRegions.pop
    protectedRegions.pop
  }

  private Int tryCatch(Catch c, Int start, Int end, ProtectedRegion region)
  {
    // assemble catch block - if there isn't a local variable
    // we emit the CatchAllStart, otherwise the block will
    // start off with a LocalVarDef which will write out the
    // CatchErrStart opcode
    handler := mark
    c.start = mark
    if (c.errVariable == null) op(FOp.CatchAllStart)
    block(c.block)
    done := -1
    if (!c.block.isExit)
    {
      if (region.hasFinally)
        region.jumpFinallys.add(jump(FOp.JumpFinally))

      done = jump(FOp.Leave)
    }
    c.end = mark
    op(FOp.CatchEnd)

    // fill in err table
    addToErrTable(start, end, handler, c.errType)

    // return position to backpatch
    return done
  }

  private Void addToErrTable(Int start, Int end, Int handler, CType? errType)
  {
    // catch all is implicitly a catch for sys::Err
    if (errType == null) errType = ns.errType

    // add to err table buffer
    errCount++
    errTable.writeI2(start)
    errTable.writeI2(end)
    errTable.writeI2(handler)
    errTable.writeI2(fpod.addTypeRef(errType))
  }

//////////////////////////////////////////////////////////////////////////
// Expressions
//////////////////////////////////////////////////////////////////////////

  Void expr(Expr expr)
  {
    line(expr.loc)
    switch (expr.id)
    {
      case ExprId.nullLiteral:     nullLiteral
      case ExprId.trueLiteral:
      case ExprId.falseLiteral:    boolLiteral(expr)
      case ExprId.intLiteral:      intLiteral(expr)
      case ExprId.floatLiteral:    floatLiteral(expr)
      case ExprId.decimalLiteral:  decimalLiteral(expr)
      case ExprId.strLiteral:      strLiteral(expr)
      case ExprId.durationLiteral: durationLiteral(expr)
      case ExprId.uriLiteral:      uriLiteral(expr)
      case ExprId.typeLiteral:     typeLiteral(expr)
      case ExprId.slotLiteral:     slotLiteral(expr)
      case ExprId.rangeLiteral:    rangeLiteral(expr)
      case ExprId.listLiteral:     listLiteral(expr)
      case ExprId.mapLiteral:      mapLiteral(expr)
      case ExprId.boolNot:         not(expr)
      case ExprId.cmpNull:         cmpNull(expr)
      case ExprId.cmpNotNull:      cmpNotNull(expr)
      case ExprId.elvis:           elvis(expr)
      case ExprId.assign:          assign(expr)
      case ExprId.same:            same(expr)
      case ExprId.notSame:         notSame(expr)
      case ExprId.boolOr:          or(expr, null)
      case ExprId.boolAnd:         and(expr, null)
      case ExprId.isExpr:          isExpr(expr)
      case ExprId.isnotExpr:       isnotExpr(expr)
      case ExprId.asExpr:          asExpr(expr)
      case ExprId.localVar:
      case ExprId.thisExpr:
      case ExprId.superExpr:
      case ExprId.itExpr:          loadLocalVar(expr)
      case ExprId.call:
      case ExprId.construction:    call(expr)
      case ExprId.shortcut:        shortcut(expr)
      case ExprId.field:           loadField(expr)
      case ExprId.coerce:          coerce(expr)
      case ExprId.closure:         closure(expr)
      case ExprId.ternary:         ternary(expr)
      case ExprId.staticTarget:    return
      case ExprId.throwExpr:       throwOp(((ThrowExpr)expr).exception)
      default:                     throw err("${expr.id} - ${expr.toStr}", expr.loc)
    }
  }

//////////////////////////////////////////////////////////////////////////
// Literals
//////////////////////////////////////////////////////////////////////////

  private Void nullLiteral()
  {
    op(FOp.LoadNull)
  }

  private Void boolLiteral(LiteralExpr expr)
  {
    if (expr.val == true)
      op(FOp.LoadTrue)
    else
      op(FOp.LoadFalse)
  }

  private Void intLiteral(LiteralExpr expr)
  {
    op(FOp.LoadInt, fpod.ints.add(expr.val))
  }

  private Void floatLiteral(LiteralExpr expr)
  {
    op(FOp.LoadFloat, fpod.floats.add(expr.val))
  }

  private Void decimalLiteral(LiteralExpr expr)
  {
    op(FOp.LoadDecimal, fpod.decimals.add(expr.val))
  }

  private Void strLiteral(LiteralExpr expr)
  {
    op(FOp.LoadStr, fpod.strs.add(expr.val))
  }

  private Void durationLiteral(LiteralExpr expr)
  {
    op(FOp.LoadDuration, fpod.durations.add(expr.val))
  }

  private Void uriLiteral(LiteralExpr expr)
  {
    op(FOp.LoadUri, fpod.uris.add(expr.val))
  }

  private Void typeLiteral(LiteralExpr expr)
  {
    val := (CType)expr.val
    op(FOp.LoadType, fpod.addTypeRef(val))
  }

  private Void slotLiteral(SlotLiteralExpr expr)
  {
    op(FOp.LoadType, fpod.addTypeRef(expr.parent))
    op(FOp.LoadStr, fpod.strs.add(expr.name))
    if (expr.slot is CField)
      op(FOp.CallVirtual, fpod.addMethodRef(ns.typeField, 1))
    else
      op(FOp.CallVirtual, fpod.addMethodRef(ns.typeMethod, 1))
  }

  private Void rangeLiteral(RangeLiteralExpr r)
  {
    expr(r.start);
    expr(r.end);
    if (r.exclusive)
      op(FOp.CallNew, fpod.addMethodRef(ns.rangeMakeExclusive))
    else
      op(FOp.CallNew, fpod.addMethodRef(ns.rangeMakeInclusive))
  }

  private Void listLiteral(ListLiteralExpr list)
  {
    t := list.ctype
    if (t is NullableType) t = t->root
    v := ((ListType)t).v

    op(FOp.LoadType, fpod.addTypeRef(v));
    op(FOp.LoadInt,  fpod.ints.add(list.vals.size))
    op(FOp.CallNew,  fpod.addMethodRef(ns.listMake))

    add := fpod.addMethodRef(ns.listAdd)
    for (i:=0; i<list.vals.size; ++i)
    {
      expr(list.vals[i])
      op(FOp.CallVirtual, add)
    }
  }

  private Void mapLiteral(MapLiteralExpr map)
  {
    op(FOp.LoadType, fpod.addTypeRef(map.ctype))
    op(FOp.CallNew,  fpod.addMethodRef(ns.mapMake))

    set := fpod.addMethodRef(ns.mapSet)
    for (i:=0; i<map.keys.size; ++i)
    {
      expr(map.keys[i])
      expr(map.vals[i])
      op(FOp.CallVirtual, set)
    }
  }

//////////////////////////////////////////////////////////////////////////
// UnaryExpr
//////////////////////////////////////////////////////////////////////////

  private Void not(UnaryExpr unary)
  {
    expr(unary.operand)
    op(FOp.CallVirtual, fpod.addMethodRef(ns.boolNot))
  }

  private Void cmpNull(UnaryExpr unary)
  {
    expr(unary.operand)
    opType(FOp.CmpNull, unary.operand.ctype)
  }

  private Void cmpNotNull(UnaryExpr unary)
  {
    expr(unary.operand)
    opType(FOp.CmpNotNull, unary.operand.ctype)
  }

//////////////////////////////////////////////////////////////////////////
// BinaryExpr
//////////////////////////////////////////////////////////////////////////

  private Void same(BinaryExpr binary)
  {
    if (binary.lhs.id === ExprId.nullLiteral ||
        binary.rhs.id === ExprId.nullLiteral)
      err("Unexpected use of same with null literals", binary.loc)
    expr(binary.lhs)
    expr(binary.rhs)
    op(FOp.CmpSame)
  }

  private Void notSame(BinaryExpr binary)
  {
    if (binary.lhs.id === ExprId.nullLiteral ||
        binary.rhs.id === ExprId.nullLiteral)
      err("Unexpected use of same with null literals", binary.loc)
    expr(binary.lhs)
    expr(binary.rhs)
    op(FOp.CmpNotSame)
  }

//////////////////////////////////////////////////////////////////////////
// CondExpr
//////////////////////////////////////////////////////////////////////////

  private Void cond(CondExpr expr, Cond cond)
  {
    switch (expr.id)
    {
      case ExprId.boolOr:  or(expr, cond)
      case ExprId.boolAnd: and(expr, cond)
      default:             throw Err(expr.id.toStr)
    }
  }

  private Void or(CondExpr expr, Cond? cond)
  {
    // if cond is null this is a top level expr which means
    // the result is to push true or false onto the stack;
    // otherwise our only job is to do the various jumps if
    // true or fall-thru if true (used with if statement)
    // NOTE: this code could be further optimized because
    //   it doesn't optimize "a && b || c && c"
    topLevel := cond == null
    if (topLevel) cond = Cond.make

    // perform short circuit logical-or
    expr.operands.each |Expr operand, Int i|
    {
      this.expr(operand)
      if (i < expr.operands.size-1)
        cond.jumpTrues.add(jump(FOp.JumpTrue))
      else
        cond.jumpFalses.add(jump(FOp.JumpFalse))
    }

    // if top level push true/false onto stack
    if (topLevel) condEnd(cond)
  }

  private Void and(CondExpr expr, Cond? cond)
  {
    // if cond is null this is a top level expr which means
    // the result is to push true or false onto the stack;
    // otherwise our only job is to do the various jumps if
    // true or fall-thru if true (used with if statement)
    // NOTE: this code could be further optimized because
    //   it doesn't optimize "a && b || c && c"
    topLevel := cond == null
    if (topLevel) cond = Cond.make

    // perform short circuit logical-and
    expr.operands.each |Expr operand|
    {
      this.expr(operand)
      cond.jumpFalses.add(jump(FOp.JumpFalse))
    }

    // if top level push true/false onto stack
    if (topLevel) condEnd(cond)
  }

  private Void condEnd(Cond cond)
  {
    // true if always fall-thru
    cond.jumpTrues.each |Int pos| { backpatch(pos) }
    op(FOp.LoadTrue)
    end := jump(FOp.Jump)

    // false
    cond.jumpFalses.each |Int pos| { backpatch(pos) }
    op(FOp.LoadFalse)

    backpatch(end)
  }

//////////////////////////////////////////////////////////////////////////
// Type Checks
//////////////////////////////////////////////////////////////////////////

  private Void isExpr(TypeCheckExpr tc)
  {
    expr(tc.target)
    op(FOp.Is, fpod.addTypeRef(tc.check))
  }

  private Void isnotExpr(TypeCheckExpr tc)
  {
    isExpr(tc)
    op(FOp.CallVirtual, fpod.addMethodRef(ns.boolNot))
  }

  private Void asExpr(TypeCheckExpr tc)
  {
    expr(tc.target)
    op(FOp.As, fpod.addTypeRef(tc.check))
  }

  private Void coerce(TypeCheckExpr tc)
  {
    expr(tc.target)
    coerceOp(tc.from, tc.ctype)
    if (!tc.leave) opType(FOp.Pop, tc.ctype)
  }

  private Void coerceOp(CType from, CType to)
  {
    // map from/to to typeRefs
    fromRef := fpod.addTypeRef(from)
    toRef   := fpod.addTypeRef(to)

    // short circuit if coercing same types
    if (fromRef == toRef) return

    // write opcode with its from/to arguments
    op(FOp.Coerce)
    code.writeI2(fromRef)
    code.writeI2(toRef)
  }

//////////////////////////////////////////////////////////////////////////
// Elvis
//////////////////////////////////////////////////////////////////////////

  private Void elvis(BinaryExpr binary)
  {
    expr(binary.lhs)
    opType(FOp.Dup, binary.lhs.ctype)
    opType(FOp.CmpNull, binary.lhs.ctype)
    isNullLabel := jump(FOp.JumpTrue)
    endLabel := jump(FOp.Jump)
    backpatch(isNullLabel)
    opType(FOp.Pop, binary.lhs.ctype)
    expr(binary.rhs)
    backpatch(endLabel)
  }

//////////////////////////////////////////////////////////////////////////
// Ternary
//////////////////////////////////////////////////////////////////////////

  private Void ternary(TernaryExpr ternary)
  {
    expr(ternary.condition)
    falseLabel := jump(FOp.JumpFalse)
    expr(ternary.trueExpr)
    endLabel := jump(FOp.Jump)
    backpatch(falseLabel)
    expr(ternary.falseExpr)
    backpatch(endLabel)
  }

//////////////////////////////////////////////////////////////////////////
// Closure
//////////////////////////////////////////////////////////////////////////

  private Void closure(ClosureExpr c)
  {
    // we replace the closure with its substitute
    // expression - call to closure constructor
    expr(c.substitute)
  }

//////////////////////////////////////////////////////////////////////////
// Assign
//////////////////////////////////////////////////////////////////////////

  **
  ** Simple assignment using =
  **
  private Void assign(BinaryExpr expr)
  {
    switch (expr.lhs.id)
    {
      case ExprId.localVar: assignLocalVar(expr)
      case ExprId.field:    assignField(expr)
      default: throw err("Internal compiler error", expr.loc)
    }
  }

//////////////////////////////////////////////////////////////////////////
// Local Var
//////////////////////////////////////////////////////////////////////////

  private Void loadLocalVar(LocalVarExpr var)
  {
    op(FOp.LoadVar, var.register)
  }

  private Void storeLocalVar(LocalVarExpr var)
  {
    op(FOp.StoreVar, var.register);
  }

  private Void assignLocalVar(BinaryExpr assign)
  {
    expr(assign.rhs)
    if (assign.leave) opType(FOp.Dup, assign.ctype)
    storeLocalVar((LocalVarExpr)assign.lhs)
  }

//////////////////////////////////////////////////////////////////////////
// Field
//////////////////////////////////////////////////////////////////////////

  private Void loadField(FieldExpr fexpr, Bool dupTarget := false)
  {
    field := fexpr.field

    // evaluate target expression
    if (fexpr.target != null)
    {
      expr(fexpr.target);
      if (dupTarget) opType(FOp.Dup, fexpr.target.ctype)
    }

    // if safe, check for null condition
    Int? isNullLabel := null
    if (fexpr.isSafe)
    {
      if (fexpr.target == null) throw err("Compiler error field isSafe", fexpr.loc)
      opType(FOp.Dup, fexpr.ctype)
      opType(FOp.CmpNull, fexpr.ctype)
      isNullLabel = jump(FOp.JumpTrue)
    }

    // load field via accessor method
    if (fexpr.useAccessor)
    {
      getter := field.getter // if null then bug in useAccessor
      index := fpod.addMethodRef(getter)
      if (field.parent.isMixin)
      {
        if (getter.isStatic)
          op(FOp.CallMixinStatic, index)
        else
          op(FOp.CallMixinVirtual, index)
      }
      else
      {
        if (getter.isStatic)
          op(FOp.CallStatic, index)
        else if (fexpr.target.id == ExprId.superExpr)
          op(FOp.CallNonVirtual, index)
        else
          op(FOp.CallVirtual, index)
      }

      // if parameterized or covariant, then coerce
      if (field.isParameterized)
        coerceOp(ns.objType, field.fieldType)
      else if (field.isCovariant)
        coerceOp(field.inheritedReturnType, field.fieldType)
    }
    // load field directly from storage
    else
    {
      index := fpod.addFieldRef(field)
      if (field.parent.isMixin)
      {
        if (field.isStatic)
          op(FOp.LoadMixinStatic, index)
        else
          throw err("LoadMixinInstance", fexpr.loc)
      }
      else
      {
        if (field.isStatic)
          op(FOp.LoadStatic, index)
        else
          op(FOp.LoadInstance, index)
      }
    }

    // if safe, handle null case
    if (fexpr.isSafe)
    {
      if (field.fieldType.isVal) coerceOp(field.fieldType, field.fieldType.toNullable)
      endLabel := jump(FOp.Jump)
      backpatch(isNullLabel)
      opType(FOp.Pop, fexpr.ctype)
      op(FOp.LoadNull)
      backpatch(endLabel)
    }
  }

  private Void assignField(BinaryExpr assign)
  {
    lhs := (FieldExpr)assign.lhs
    isInstanceField := !lhs.field.isStatic;  // used to determine how to duplicate

    if (lhs .target != null) expr(lhs.target)
    expr(assign.rhs);
    if (assign.leave)
    {
      opType(FOp.Dup, assign.ctype)
      if (isInstanceField)
        op(FOp.StoreVar, assign.tempVar.register)
    }
    storeField(lhs)
    if (assign.leave && isInstanceField)
    {
      op(FOp.LoadVar, assign.tempVar.register)
    }
  }

  private Void storeField(FieldExpr fexpr)
  {
    field := fexpr.field
    if (fexpr.useAccessor)
    {
      setter := field.setter  // if null then bug in useAccessor
      index := fpod.addMethodRef(setter)

      if (field.parent.isMixin) // TODO
      {
        if (setter.isStatic)
          op(FOp.CallMixinStatic, index)
        else
          op(FOp.CallMixinVirtual, index)
      }
      else
      {
        if (setter.isStatic)
          op(FOp.CallStatic, index)
        else if (fexpr.target.id == ExprId.superExpr)
          op(FOp.CallNonVirtual, index)
        else
          op(FOp.CallVirtual, index)
      }
    }
    else
    {
      index := fpod.addFieldRef(field)

      if (field.parent.isMixin)
      {
        if (field.isStatic)
          op(FOp.StoreMixinStatic, index)
        else
          throw err("StoreMixinInstance", fexpr.loc)
      }
      else
      {
        if (field.isStatic)
          op(FOp.StoreStatic, index)
        else
          op(FOp.StoreInstance, index)
      }
    }
  }

//////////////////////////////////////////////////////////////////////////
// Call
//////////////////////////////////////////////////////////////////////////

  private Void call(CallExpr call, Bool leave := call.leave)
  {
    // evaluate target
    method := call.method

    // push call target onto the stack
    target := call.target
    if (target != null) expr(target)

    // if safe, check for null
    Int? isNullLabel := null
    if (call.isSafe)
    {
      // sanity check
      if (target == null || (target.ctype.isVal && !target.ctype.isNullable))
        throw err("Compiler error call isSafe: $call", call.loc)

      // check if null and if so then jump over call
      opType(FOp.Dup, target.ctype)
      opType(FOp.CmpNull, target.ctype)
      isNullLabel = jump(FOp.JumpTrue)

      // now if we are calling a value-type method we might need to coerce
      if (target.ctype.isVal || method.parent.isVal)
        coerceOp(target.ctype, call.method.parent)
    }

    // invoke call
    if (call.isDynamic)
    {
      dynamicCall(call)
    }
    else
    {
      call.args.each |Expr arg| { expr(arg) }
      invokeCall(call, leave)
    }

    // if safe, handle null case
    if (call.isSafe)
    {
      // if the method return a value type, ensure it is coerced to nullable
      if (method.returnType.isVal && call.leave)
        coerceOp(method.returnType, call.ctype.toNullable)

      // jump to end after successful call and push null onto
      // stack for null check from above (if a leave)
      endLabel := jump(FOp.Jump)
      backpatch(isNullLabel)
      opType(FOp.Pop, target.ctype)
      if (call.leave) op(FOp.LoadNull)
      backpatch(endLabel)
    }
  }

  private Void dynamicCall(CallExpr call)
  {
    // name str literal
    op(FOp.LoadStr, fpod.strs.add(call.name))

    // args Obj[]
    if (call.args.isEmpty)
    {
      op(FOp.LoadNull)
    }
    else
    {
      op(FOp.LoadInt,  fpod.ints.add(call.args.size))
      op(FOp.CallNew,  fpod.addMethodRef(ns.listMakeObj))
      add := fpod.addMethodRef(ns.listAdd)
      call.args.each |Expr arg|
      {
        expr(arg)
        op(FOp.CallVirtual, add)
      }
    }

    // Obj.trap
    op(FOp.CallVirtual, fpod.addMethodRef(ns.objTrap))

    // pop return if no leave
    if (!call.leave) opType(FOp.Pop, call.ctype)
  }

  private Void invokeCall(CallExpr call, Bool leave := call.leave)
  {
    m := call.method
    index := fpod.addMethodRef(m, call.args.size)

    // write CallVirtual, CallNonVirtual, CallStatic, CallNew, or CallCtor;
    // note that if a constructor call has a target (this or super), then it
    // is a CallCtor instance call because we don't want to allocate
    // a new instance
    if (m.parent.isMixin)
    {
      if (m.isStatic)
        op(FOp.CallMixinStatic, index)
      else if (call.target.id == ExprId.superExpr)
        op(FOp.CallMixinNonVirtual, index)
      else
        op(FOp.CallMixinVirtual, index)
    }
    else if (m.isStatic)
    {
      op(FOp.CallStatic, index)
    }
    else if (m.isCtor)
    {
      if (call.target == null || call.target.id == ExprId.staticTarget)
        op(FOp.CallNew, index)
      else
        op(FOp.CallCtor, index)
    }
    else
    {
      // because CallNonVirtual maps to Java's invokespecial, we can't
      // use it for calls outside of the class (consider it like calling
      // protected method); we also don't want to use non-virtual for
      // any Obj methods since those are implemented as static wrappers
      // in the Java/.NET runtime
      targetId := call.target.id
      if (targetId == ExprId.superExpr ||
          (targetId == ExprId.thisExpr && !m.isVirtual && !m.parent.isObj))
        op(FOp.CallNonVirtual, index)
      else
        op(FOp.CallVirtual, index)
    }

    // if we are leaving a value on the stack of a method which
    // has a parameterized return value or is covariant, then we
    // need to insert a cast operation
    //   Int.toStr    => non-generic - no cast
    //   Str[].toStr  => return isn't parameterized - no cast
    //   Str[].get()  => actual return is Obj, but we want Str - cast
    //   covariant    => actual call is against inheritedReturnType
    if (leave)
    {
      if (m.isParameterized)
      {
        ret := m.generic.returnType
        if (ret.isGenericParameter)
          coerceOp(ns.objType, m.returnType)
      }
      else if (m.isCovariant)
      {
        coerceOp(m.inheritedReturnType, m.returnType)
      }
    }

    // if the method left a value on the stack, and we
    // aren't going to use it, then pop it off
    if (!leave)
    {
      // note we need to use the actual method signature (not parameterized)
      x := m.isParameterized ? m.generic : m
      if (!x.returnType.isVoid || x.isInstanceCtor)
        opType(FOp.Pop, x.returnType)
    }
  }

//////////////////////////////////////////////////////////////////////////
// Shortcut
//////////////////////////////////////////////////////////////////////////

  private Void shortcut(ShortcutExpr call)
  {
    // handle comparisions as special opcodes
    target := call.target
    firstArg := call.args.first
    switch (call.opToken)
    {
      case Token.eq:     compareOp(target, FOp.CmpEQ, firstArg); return
      case Token.notEq:  compareOp(target, FOp.CmpNE, firstArg); return
      case Token.cmp:    compareOp(target, FOp.Cmp,   firstArg); return
      case Token.lt:     compareOp(target, FOp.CmpLT, firstArg); return
      case Token.ltEq:   compareOp(target, FOp.CmpLE, firstArg); return
      case Token.gt:     compareOp(target, FOp.CmpGT, firstArg); return
      case Token.gtEq:   compareOp(target, FOp.CmpGE, firstArg); return
    }

    // always check string concat first since it can
    // have string on either left or right hand side
    if (call.isStrConcat)
    {
      addStr(call, true)
      return
    }

    // if assignment we need to do a bunch of special processing
    if (call.isAssign)
    {
      shortcutAssign(call)
      return
    }

    // just process as normal call
    this.call(call)
  }

  **
  ** Generate a comparison.  The lhs can be either a ctype or an expr.
  **
  private Void compareOp(Obj lhs, FOp opCode, Expr rhs)
  {
    lhsExpr := lhs as Expr
    lhsType := lhsExpr != null ? lhsExpr.ctype : (CType)lhs

    if (lhsExpr != null) expr(lhsExpr)
    expr(rhs)

    fromRef := fpod.addTypeRef(lhsType)
    toRef   := fpod.addTypeRef(rhs.ctype)

    op(opCode)
    code.writeI2(fromRef)
    code.writeI2(toRef)
  }

  **
  ** This method is used for complex assignments: prefix/postfix
  ** increment and special dual assignment operators like "+=".
  **
  private Void shortcutAssign(ShortcutExpr c)
  {
    var := c.target
    leaveUsingTemp := false

    // if var is a coercion set that aside and get real variable
    TypeCheckExpr? coerce := null
    if (var.id == ExprId.coerce)
    {
      coerce = (TypeCheckExpr)var
      var = coerce.target
    }

    // load the variable
    switch (var.id)
    {
      case ExprId.localVar:
        loadLocalVar((LocalVarExpr)var)
      case ExprId.field:
        fexpr := (FieldExpr)var
        loadField(fexpr, true) // dup target on stack for upcoming set
        leaveUsingTemp = !fexpr.field.isStatic  // used to determine how to duplicate
      case ExprId.shortcut:
        // since .NET sucks when it comes to stack manipulation,
        // we use two scratch locals to get the stack into the
        // following format:
        //   index  \  used for get
        //   target /
        //   index  \  used for set
        //   target /
        index := (IndexedAssignExpr)c
        get := (ShortcutExpr)var
        expr(get.target)  // target
        opType(FOp.Dup, get.target.ctype)
        op(FOp.StoreVar, index.scratchA.register)
        expr(get.args[0]) // index expr
        opType(FOp.Dup, get.args[0].ctype)
        op(FOp.StoreVar, index.scratchB.register)
        op(FOp.LoadVar, index.scratchA.register)
        op(FOp.LoadVar, index.scratchB.register)
        invokeCall(get, true)
        leaveUsingTemp = true
      default:
        throw err("Internal error", var.loc)
    }

    // if we have a coercion do it
    if (coerce != null) coerceOp(var.ctype, coerce.check)

    // if postfix leave, duplicate value before we preform computation
    if (c.leave && c.isPostfixLeave)
    {
      opType(FOp.Dup, c.ctype)
      if (leaveUsingTemp)
        op(FOp.StoreVar, c.tempVar.register)
    }

    // load args and invoke call
    c.args.each |Expr arg| { expr(arg) }
    invokeCall(c, true)

    // if prefix, duplicate after we've done computation
    if (c.leave && !c.isPostfixLeave)
    {
      opType(FOp.Dup, c.ctype)
      if (leaveUsingTemp)
        op(FOp.StoreVar, c.tempVar.register)
    }

    // if we have a coercion then uncoerce,
    // otherwise perform coerce to ensure we
    // have right type to store back to variable
    if (coerce != null) coerceOp(coerce.check, var.ctype)
    else coerceOp(c.ctype, var.ctype)

    // save the variable back
    switch (var.id)
    {
      case ExprId.localVar:
        storeLocalVar((LocalVarExpr)var)
      case ExprId.field:
        storeField((FieldExpr)var)
      case ExprId.shortcut:
        set := (CMethod)c->setMethod
        setParam := (set.isParameterized ? set.generic : set).params[1].paramType
        // if calling setter check if we need to boxed
        if (c.ctype.isVal && !setParam.isVal && coerce == null) coerceOp(c.ctype, setParam)
        op(FOp.CallVirtual, fpod.addMethodRef(set, 2))
        if (!set.returnType.isVoid) opType(FOp.Pop, set.returnType)
      default:
        throw err("Internal error", var.loc)
    }

    // if field leave, then load back from temp local
    if (c.leave && leaveUsingTemp)
      op(FOp.LoadVar, c.tempVar.register)
  }

//////////////////////////////////////////////////////////////////////////
// Strings
//////////////////////////////////////////////////////////////////////////

  **
  ** Assemble code to build a string using sys::StrBuf.
  **
  private Void addStr(ShortcutExpr expr, Bool topLevel)
  {
    if (topLevel)
      op(FOp.CallNew, fpod.addMethodRef(ns.strBufMake, 0))

    lhs := expr.target
    rhs := expr.args.first

    lhsShortcut := lhs as ShortcutExpr
    if (lhsShortcut != null && lhsShortcut.isStrConcat)
    {
      addStr(lhsShortcut, false)
    }
    else
    {
      if (!isEmptyStrLiteral(lhs))
      {
        this.expr(lhs)
        if (lhs.ctype.isVal) coerceOp(lhs.ctype, ns.objType)
        op(FOp.CallVirtual, fpod.addMethodRef(ns.strBufAdd))
      }
    }

    if (!isEmptyStrLiteral(rhs))
    {
      this.expr(rhs)
      op(FOp.CallVirtual, fpod.addMethodRef(ns.strBufAdd))
    }

    if (topLevel) op(FOp.CallVirtual, fpod.addMethodRef(ns.strBufToStr))
  }

  private Bool isEmptyStrLiteral(Expr expr)
  {
    return expr.id === ExprId.strLiteral && expr->val == ""
  }

//////////////////////////////////////////////////////////////////////////
// Code Buffer
//////////////////////////////////////////////////////////////////////////

  **
  ** Append a opcode with a type argument.
  **
  Void opType(FOp opcode, CType arg)
  {
    op(opcode, fpod.addTypeRef(arg))
  }

  **
  ** Append a opcode with option two byte argument.
  **
  Void op(FOp op, Int? arg := null)
  {
    code.write(op.ordinal)
    if (arg != null) code.writeI2(arg)
  }

//////////////////////////////////////////////////////////////////////////
// Jumps
//////////////////////////////////////////////////////////////////////////

  **
  ** Get the current location as a mark to use for backwards jump.
  **
  private Int mark()
  {
    return code.size
  }

  **
  ** Add the specified jump opcode and two bytes for the jump
  ** location.  If a backward jump then pass the mark; if a
  ** a forward jump we return the code pos to backpatch the
  ** mark later.
  **
  private Int jump(FOp op, Int mark := 0xffff)
  {
    this.op(op, mark)
    return code.size-2
  }

  **
  ** Backpacth the mark of forward jump using the given
  ** pos which was returned by jump().  If mark is defaulted,
  ** then we use the current instruction as the mark.
  **
  private Void backpatch(Int pos, Int mark := code.size)
  {
    orig := code.pos
    code.seek(pos).writeI2(mark)
    code.seek(orig)
  }

//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////

  **
  ** Finish writing out the exception handling table
  **
  Buf finishCode()
  {
    // if we had to return from a protected region, then now we
    // need to generate the actual return instructions and backpatch
    // all the leaves
    if (leavesToReturn != null)
    {
      leavesToReturn.each |Int pos| { backpatch(pos) }
      if (returnLocal != null) op(FOp.LoadVar, returnLocal.register)
      op(FOp.Return)
    }

    // check final size
    if (code.size >= 0x7fff) throw err("Method too big", loc)
    return code
  }

  **
  ** Finish writing out the exception handling table
  **
  Buf finishErrTable()
  {
    errTable.seek(0).writeI2(errCount)
    return errTable
  }

  **
  ** Finish writing out the line number table
  **
  Buf finishLines()
  {
    lines.seek(0).writeI2(lineCount)
    return lines
  }

  **
  ** Map the opcode we are getting ready to add to the specified line number
  **
  private Void line(Loc loc)
  {
    line   := loc.line
    offset := code.size
    if (line == null || lastLine == line || lastOffset == offset) return
    lineCount++
    lines.writeI2(offset)
    lines.writeI2(line)
    lastLine = line
    lastOffset = offset
  }

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

  Loc loc
  FPod fpod
  MethodDef? curMethod
  Buf code
  Buf errTable
  Int errCount
  Buf lines
  Int lineCount
  Int lastLine := -1
  Int lastOffset := -1
  Loop[] loopStack

  // protected region fields
  ProtectedRegion[]? protectedRegions // stack of protection regions
  Int[]? leavesToReturn    // list of Leave positions to backpatch
  MethodVar? returnLocal    // where we stash return value
}

**************************************************************************
** Loop
**************************************************************************

class Loop
{
  new make(Stmt stmt) { this.stmt = stmt }

  Stmt stmt                  // WhileStmt or ForStmt
  Int[] breaks := Int[,]     // backpatch positions
  Int[] continues := Int[,]  // backpatch positions
  ProtectedRegion[] protectedRegions := ProtectedRegion[,] // stack
}

**************************************************************************
** ProtectedRegion
**************************************************************************

class ProtectedRegion
{
  new make(TryStmt stmt)
  {
    hasFinally = stmt.finallyBlock != null
    if (hasFinally) jumpFinallys = Int[,]
  }

  Bool hasFinally      // does this region have a finally
  Int[]? jumpFinallys  // list of JumpFinally positions to backpatch
}

**************************************************************************
** Cond
**************************************************************************

class Cond
{
  Int[] jumpTrues  := Int[,]   // backpatch positions
  Int[] jumpFalses := Int[,]   // backpatch positions
}