//
// Copyright (c) 2006, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   19 Jul 06  Brian Frank  Creation
//

**
** Expr
**
abstract class Expr : Node
{

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

  new make(Loc loc, ExprId id)
    : super(loc)
  {
    this.id = id
  }

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

  **
  ** Return this expression as an Int literal usable in a tableswitch,
  ** or null if this Expr doesn't represent a constant Int.  Expressions
  ** which work as table switch cases: int literals and enum constants
  **
  virtual Int? asTableSwitchCase() { null }

  **
  ** Get this expression's type as a string for error reporting.
  **
  Str toTypeStr()
  {
    if (id == ExprId.nullLiteral) return "null"
    return ctype.toStr
  }

  **
  ** If this expression performs assignment, then return
  ** the target of that assignment.  Otherwise return null.
  **
  virtual Obj? assignTarget() { null }

  **
  ** Return if this expression can be used as the
  ** left hand side of an assignment expression.
  **
  virtual Bool isAssignable() { false }

  **
  ** Is this a boolean conditional (boolOr/boolAnd)
  **
  virtual Bool isCond() { false }

  **
  ** Does this expression make up a complete statement.
  ** If you override this to true, then you must make sure
  ** the expr is popped in CodeAsm.
  **
  virtual Bool isStmt() { false }

  **
  ** Was this expression generated by the compiler (not necessarily
  ** everything auto-generated has this flag true, but we set in
  ** cases where error checking needs to be handled special)
  **
  virtual Bool synthetic() { false }

  **
  ** If this an assignment expression, then return the
  ** result of calling the given function with the LHS.
  ** Otherwise return false.
  **
  virtual Bool isDefiniteAssign(|Expr lhs->Bool| f) { false }

  **
  ** Return if this expression is guaranteed to sometimes
  ** return a null result (safe invoke, as, etc)
  **
  virtual Bool isAlwaysNullable() { false }

  **
  ** Assignments to instance fields require a temporary local variable.
  **
  virtual Bool assignRequiresTempVar() { false }

  **
  ** Return if this expression represents the same variable or
  ** field as that.  This is used for self assignment checks.
  **
  virtual Bool sameVarAs(Expr that) { false }

  **
  ** Map the list of expressions into their list of types
  **
  static CType[] ctypes(Expr[] exprs)
  {
    return exprs.map |Expr e->CType| { e.ctype }
  }

  **
  ** Given a list of Expr instances, find the common base type
  ** they all share.  This method does not take into account
  ** the null literal.  It is used for type inference for lists
  ** and maps.
  **
  static CType commonType(CNamespace ns, Expr[] exprs)
  {
    hasNull := false
    exprs = exprs.exclude |Expr e->Bool|
    {
      if (e.id !== ExprId.nullLiteral) return false
      hasNull = true
      return true
    }
    t := CType.common(ns, ctypes(exprs))
    if (hasNull) t = t.toNullable
    return t
  }

  **
  ** Return this expression as an ExprStmt
  **
  ExprStmt toStmt()
  {
    return ExprStmt(this)
  }

  **
  ** Return this expression as serialization text or
  ** throw exception if not serializable.
  **
  virtual Str serialize()
  {
    throw CompilerErr("'$id' not serializable", loc)
  }

  **
  ** Make an Expr which will serialize the given literal.
  **
  static Expr makeForLiteral(Loc loc, CNamespace ns, Obj val)
  {
    switch (val.typeof)
    {
      case Bool#:
        return val == true ?
          LiteralExpr(loc, ExprId.trueLiteral, ns.boolType, true) :
          LiteralExpr(loc, ExprId.falseLiteral, ns.boolType, false)
      case Str#:
        return LiteralExpr(loc, ExprId.strLiteral, ns.strType, val)
      case DateTime#:

        return CallExpr(loc, null, "fromStr", ExprId.construction)
        {
          method = ns.resolveSlot("sys::DateTime.fromStr")
          ctype  = method.parent
          args   = [makeForLiteral(loc, ns, val.toStr)]
        }
      default:
        throw Err("Unsupported literal type $val.typeof")
    }
  }

  **
  ** Set this expression to not be left on the stack.
  **
  Expr noLeave()
  {
    // if the expression is prefixed with a synthetic cast by
    // CallResolver, it is unnecessary at the top level and must
    // be stripped
    result := this
    if (result.id === ExprId.coerce)
    {
      coerce := (TypeCheckExpr)result
      if (coerce.synthetic) result = coerce.target
    }
    result.leave = false
    return result
  }

//////////////////////////////////////////////////////////////////////////
// Doc
//////////////////////////////////////////////////////////////////////////

  **
  ** Get this expression as a string suitable for documentation.
  ** This string must not contain a newline or it will break the
  ** DocApiParser.
  **
  Str? toDocStr()
  {
    // not perfect, but better than what we had previously which
    // was nothing; we might want to grab the actual text from the
    // actual source file - but with the current design we've freed
    // the buffer by the time the tokens are passed to the parser
    try
    {
      // literals
      if (this is LiteralExpr)
      {
        s := serialize
        if (s.size > 40) s = "..."
        return s
      }

      // if this is cast, return base
      if (this is TypeCheckExpr)
        return ((TypeCheckExpr)this).target.toDocStr

      // if we access an internal slot then don't expose in public docs
      CSlot? slot := null
      if (this is CallExpr) slot = ((CallExpr)this).method
      else if (this is FieldExpr) slot = ((FieldExpr)this).field
      if (slot != null && (slot.isPrivate || slot.isInternal)) return null

      // remove extra parens with binary ops
      s := toStr
      if (s[0] == '(' && s[-1] == ')') s = s[1..-2]

      // hide implicit assignments
      if (s.contains("=")) s = s[s.index("=")+1..-1].trim

      // remove extra parens with binary ops
      if (s[0] == '(' && s[-1] == ')' && !s.endsWith("()")) s = s[1..-2]

      // hide storage operator
      s = s.replace(".@", ".")

      // hide safe nav construction
      s = s.replace(".?(", "(")

      // use unqualified names
      while (true)
      {
        qcolon := s.index("::")
        if (qcolon == null) break
        i := qcolon-1
        for (; i>=0; --i) if (!s[i].isAlphaNum && s[i] != '_') break
        s = (i < 0) ? s[qcolon+2..-1] : s[0..i] + s[qcolon+2..-1]
      }

      if (s.size > 40) s = "..."
      return s
    }
    catch (Err e)
    {
      e.trace
      return toStr
    }
  }

//////////////////////////////////////////////////////////////////////////
// Tree
//////////////////////////////////////////////////////////////////////////

  Expr walk(Visitor v)
  {
    walkChildren(v)
    return v.visitExpr(this)
  }

  virtual Void walkChildren(Visitor v)
  {
  }

  static Expr? walkExpr(Visitor v, Expr? expr)
  {
    if (expr == null) return null
    return expr.walk(v)
  }

  static Expr[] walkExprs(Visitor v, Expr?[] exprs)
  {
    for (i:=0; i<exprs.size; ++i)
    {
      expr := exprs[i]
      if (expr != null)
      {
        replace := expr.walk(v)
        if (expr !== replace)
          exprs[i] = replace
      }
    }
    return exprs
  }

//////////////////////////////////////////////////////////////////////////
// Debug
//////////////////////////////////////////////////////////////////////////

  override abstract Str toStr()

  override Void print(AstWriter out)
  {
    out.w(toStr)
  }

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

  const ExprId id         // expression type identifier
  CType? ctype            // type expression resolves to
  Bool leave := true { protected set } // leave this expression on the stack
}

**************************************************************************
** LiteralExpr
**************************************************************************

**
** LiteralExpr puts an Bool, Int, Float, Str, Duration, Uri,
** or null constant onto the stack.
**
class LiteralExpr : Expr
{
  new make(Loc loc, ExprId id, CType ctype, Obj? val)
    : super(loc, id)
  {
    this.ctype = ctype
    this.val   = val
    if (val == null && !ctype.isNullable)
      throw Err("null literal must typed as nullable!")
  }

  new makeNull(Loc loc, CNamespace ns)
    : this.make(loc, ExprId.nullLiteral, ns.objType.toNullable, null) {}

  new makeTrue(Loc loc, CNamespace ns)
    : this.make(loc, ExprId.trueLiteral, ns.boolType, true) {}

  new makeFalse(Loc loc, CNamespace ns)
    : this.make(loc, ExprId.falseLiteral, ns.boolType, false) {}

  new makeStr(Loc loc, CNamespace ns, Str val)
    : this.make(loc, ExprId.strLiteral, ns.strType, val) {}

  static LiteralExpr makeDefaultLiteral(Loc loc, CNamespace ns, CType ctype)
  {
    if (!ctype.isNullable())
    {
      if (ctype.isBool())  return make(loc, ExprId.falseLiteral, ctype, false)
      if (ctype.isInt())   return make(loc, ExprId.intLiteral, ctype, 0)
      if (ctype.isFloat()) return make(loc, ExprId.floatLiteral, ctype, 0f)
    }
    return makeNull(loc, ns)
  }

  override Bool isAlwaysNullable() { id === ExprId.nullLiteral }

  override Int? asTableSwitchCase()
  {
    return val as Int
  }

  override Str serialize()
  {
    switch (id)
    {
      case ExprId.nullLiteral:     return "null"
      case ExprId.falseLiteral:    return "false"
      case ExprId.trueLiteral:     return "true"
      case ExprId.intLiteral:      return val.toStr
      case ExprId.floatLiteral:    return val.toStr + "f"
      case ExprId.decimalLiteral:  return val.toStr + "d"
      case ExprId.strLiteral:      return val.toStr.toCode
      case ExprId.uriLiteral:      return val.toStr.toCode('`')
      case ExprId.typeLiteral:     return "${val->signature}#"
      case ExprId.durationLiteral: return val.toStr
      default:                     return super.serialize
    }
  }

  override Str toStr()
  {
    switch (id)
    {
      case ExprId.nullLiteral: return "null"
      case ExprId.strLiteral:  return "\"" + val.toStr.replace("\n", "\\n") + "\""
      case ExprId.typeLiteral: return "${val}#"
      case ExprId.uriLiteral:  return "`$val`"
      default: return val.toStr
    }
  }

  Obj? val // Bool, Int, Float, Str (for Str/Uri), Duration, CType, or null
}

**************************************************************************
** LocaleLiteralExpr
**************************************************************************

**
** LocaleLiteralExpr: podName::key=defVal
**
class LocaleLiteralExpr: Expr
{
  new make(Loc loc, Str pattern)
    : super(loc, ExprId.localeLiteral)
  {
    this.pattern = pattern
    this.key = pattern
    eq := pattern.index("=")
    if (eq != null)
    {
      this.key = pattern[0..<eq]
      this.def = pattern[eq+1..-1]
    }

    colons := key.index("::")
    if (colons != null)
    {
      this.podName = key[0..<colons]
      this.key     = key[colons+2..-1]
    }
  }

  override Str toStr() { "<${pattern}>" }

  Str pattern
  Str key
  Str? podName
  Str? def
}

**************************************************************************
** SlotLiteralExpr
**************************************************************************

**
** SlotLiteralExpr
**
class SlotLiteralExpr : Expr
{
  new make(Loc loc, CType parent, Str name)
    : super(loc, ExprId.slotLiteral)
  {
    this.parent = parent
    this.name = name
  }

  override Str serialize() { "$parent.signature#${name}" }

  override Str toStr() { "$parent.signature#${name}" }

  CType parent
  Str name
  CSlot? slot
}

**************************************************************************
** RangeLiteralExpr
**************************************************************************

**
** RangeLiteralExpr creates a Range instance
**
class RangeLiteralExpr : Expr
{
  new make(Loc loc, CType ctype, Expr start, Expr end, Bool exclusive)
    : super(loc, ExprId.rangeLiteral)
  {
    this.ctype = ctype
    this.start = start
    this.end   = end
    this.exclusive = exclusive
  }

  override Void walkChildren(Visitor v)
  {
    start = start.walk(v)
    end   = end.walk(v)
  }

  override Str toStr()
  {
    if (exclusive)
      return "${start}...${end}"
    else
      return "${start}..${end}"
  }

  Expr start
  Expr end
  Bool exclusive
}

**************************************************************************
** ListLiteralExpr
**************************************************************************

**
** ListLiteralExpr creates a List instance
**
class ListLiteralExpr : Expr
{
  new make(Loc loc, ListType? explicitType := null)
    : super(loc, ExprId.listLiteral)
  {
    this.explicitType = explicitType
  }

  new makeFor(Loc loc, CType ctype, Expr[] vals)
    : super.make(loc, ExprId.listLiteral)
  {
    this.ctype = ctype
    this.vals  = vals
  }

  override Void walkChildren(Visitor v)
  {
    vals = walkExprs(v, vals)
  }

  override Str serialize()
  {
    return format |Expr e->Str| { e.serialize }
  }

  override Str toStr()
  {
    return format |Expr e->Str| { e.toStr }
  }

  Str format(|Expr e->Str| f)
  {
    s := StrBuf.make
    if (explicitType != null) s.add(explicitType.v)
    s.add("[")
    if (vals.isEmpty) s.add(",")
    else vals.each |Expr v, Int i|
    {
      if (i > 0) s.add(",")
      s.add(f(v))
    }
    s.add("]")
    return s.toStr
  }

  ListType? explicitType
  Expr[] vals := Expr[,]
}

**************************************************************************
** MapLiteralExpr
**************************************************************************

**
** MapLiteralExpr creates a List instance
**
class MapLiteralExpr : Expr
{
  new make(Loc loc, MapType? explicitType := null)
    : super(loc, ExprId.mapLiteral)
  {
    this.explicitType = explicitType
  }

  override Void walkChildren(Visitor v)
  {
    keys = walkExprs(v, keys)
    vals = walkExprs(v, vals)
  }

  override Str serialize()
  {
    return format |Expr e->Str| { e.serialize }
  }

  override Str toStr()
  {
    return format |Expr e->Str| { e.toStr }
  }

  Str format(|Expr e->Str| f)
  {
    s := StrBuf.make
    if (explicitType != null) s.add(explicitType)
    s.add("[")
    if (vals.isEmpty) s.add(":")
    else
    {
      keys.size.times |Int i|
      {
        if (i > 0) s.add(",")
        s.add(f(keys[i])).add(":").add(f(vals[i]))
      }
    }
    s.add("]")
    return s.toStr
  }

  MapType? explicitType
  Expr[] keys := Expr[,]
  Expr[] vals := Expr[,]
}

**************************************************************************
** UnaryExpr
**************************************************************************

**
** UnaryExpr is used for unary expressions including !, +.
** Note that - is mapped to negate() as a shortcut method.
**
class UnaryExpr : Expr
{
  new make(Loc loc, ExprId id, Token opToken, Expr operand)
    : super(loc, id)
  {
    this.opToken = opToken
    this.operand = operand
  }

  override Void walkChildren(Visitor v)
  {
    operand = operand.walk(v)
  }

  override Str toStr()
  {
    if (id == ExprId.cmpNull)
      return operand.toStr + " == null"
    else if (id == ExprId.cmpNotNull)
      return operand.toStr + " != null"
    else
      return opToken.toStr + operand.toStr
  }

  Token opToken   // operator token type (Token.bang, etc)
  Expr operand    // operand expression

}

**************************************************************************
** BinaryExpr
**************************************************************************

**
** BinaryExpr is used for binary expressions with a left hand side and a
** right hand side including assignment.  Note that many common binary
** operations are actually modeled as ShortcutExpr to enable method based
** operator overloading.
**
class BinaryExpr : Expr
{
  new make(Expr lhs, Token opToken, Expr rhs)
    : super(lhs.loc, opToken.toExprId)
  {
    this.lhs = lhs
    this.opToken = opToken
    this.rhs = rhs
  }

  new makeAssign(Expr lhs, Expr rhs, Bool leave := false)
    : this.make(lhs, Token.assign, rhs)
  {
    this.ctype = lhs.ctype
    this.leave = leave
  }

  override Obj? assignTarget() { id === ExprId.assign ? lhs : null }

  override Bool isStmt() { id === ExprId.assign }

  override Bool isDefiniteAssign(|Expr lhs->Bool| f)
  {
    if (id === ExprId.assign && f(lhs)) return true
    return rhs.isDefiniteAssign(f)
  }

  override Void walkChildren(Visitor v)
  {
    lhs = lhs.walk(v)
    rhs = rhs.walk(v)
  }

  override Str serialize()
  {
    if (id === ExprId.assign)
      return "${lhs.serialize}=${rhs.serialize}"
    else
      return super.serialize
  }

  override Str toStr()
  {
    return "($lhs $opToken $rhs)"
  }

  Token opToken      // operator token type (Token.and, etc)
  Expr lhs           // left hand side
  Expr rhs           // right hand side
  MethodVar? tempVar // temp local var to store field assignment leaves

}

**************************************************************************
** CondExpr
**************************************************************************

**
** CondExpr is used for || and && short-circuit boolean conditionals.
**
class CondExpr : Expr
{
  new make(Expr first, Token opToken)
    : super(first.loc, opToken.toExprId)
  {
    this.opToken = opToken
    this.operands = [first]
  }

  override Bool isCond() { true }

  override Void walkChildren(Visitor v)
  {
    operands = walkExprs(v, operands)
  }

  override Str toStr()
  {
    return operands.join(" $opToken ")
  }

  Token opToken      // operator token type (Token.and, etc)
  Expr[] operands    // list of operands

}

**************************************************************************
** NameExpr
**************************************************************************

**
** NameExpr is the base class for an identifier expression which has
** an optional base expression.  NameExpr is the base class for
** UnknownVarExpr and CallExpr which are resolved via CallResolver
**
abstract class NameExpr : Expr
{
  new make(Loc loc, ExprId id, Expr? target, Str? name)
    : super(loc, id)
  {
    this.target = target
    this.name   = name
    this.isSafe = false
  }

  override Bool isAlwaysNullable() { isSafe }

  override Void walkChildren(Visitor v)
  {
    target = walkExpr(v, target)
  }

  override Str toStr()
  {
    if (target != null)
      return target.toStr + (isSafe ? "?." : ".") + name
    else
      return name
  }

  Expr? target  // base target expression or null
  Str? name     // name of variable (local/field/method)
  Bool isSafe   // if ?. operator
}

**************************************************************************
** UnknownVarExpr
**************************************************************************

**
** UnknownVarExpr is a place holder in the AST for a variable until
** we can figure out what it references: local or slot.  We also use
** this class for storage operators before they are resolved to a field.
**
class UnknownVarExpr : NameExpr
{
  new make(Loc loc, Expr? target, Str name, ExprId id := ExprId.unknownVar)
    : super(loc, id, target, name)
  {
  }
}

**************************************************************************
** CallExpr
**************************************************************************

**
** CallExpr is a method call.
**
class CallExpr : NameExpr
{
  new make(Loc loc, Expr? target := null, Str? name := null, ExprId id := ExprId.call)
    : super(loc, id, target, name)
  {
    args = Expr[,]
    isDynamic = false
    isSafe = false
    isCtorChain = false
  }

  new makeWithMethod(Loc loc, Expr? target, CMethod method, Expr[]? args := null)
    : this.make(loc, target, method.name, ExprId.call)
  {
    this.method = method
    this.ctype = method.isCtor ? method.parent : method.returnType
    if (args != null) this.args = args
  }

  override Str toStr()
  {
    return toCallStr(true)
  }

  override Bool isDefiniteAssign(|Expr lhs->Bool| f)
  {
    if (target != null && target.isDefiniteAssign(f)) return true
    return args.any |Expr arg->Bool| { arg.isDefiniteAssign(f) }
  }

  override Bool isStmt()
  {
    // stand alone constructor is not a valid stmt
    if (method.isCtor) return false

    // with block applied to stand alone constructor is not valid stmt
    if (method.name == "with" && target is CallExpr && ((CallExpr)target).method.isCtor)
      return false

    // consider any other call a stand alone stmt
    return true
  }

  virtual Bool isCompare() { false }

  override Void walkChildren(Visitor v)
  {
    target = walkExpr(v, target)
    args = walkExprs(v, args)
  }

  override Str serialize()
  {
    // only serialize a true Type("xx") expr which maps to Type.fromStr
    if (id != ExprId.construction || method.name != "fromStr")
      return super.serialize

    argSer := args.join(",") |Expr e->Str| { e.serialize }
    return "$method.parent($argSer)"
  }

  override Void print(AstWriter out)
  {
    out.w(toCallStr(false))
    if (args.size > 0 && args.last is ClosureExpr)
      args.last.print(out)
  }

  private Str toCallStr(Bool isToStr)
  {
    s := StrBuf.make

    if (target != null)
    {
      s.add(target).add(isSafe ? "?" : "").add(isDynamic ? "->" : ".")
    }
    else if (method != null && (method.isStatic || method.isCtor))
      s.add(method.parent.qname).add(".")

    s.add(name).add("(")
    if (args.last is ClosureExpr)
    {
      s.add(args[0..-2].join(", ")).add(") ");
      if (isToStr) s.add(args.last)
    }
    else
    {
      s.add(args.join(", ")).add(")")
    }
    return s.toStr
  }

  Expr[] args         // Expr[] arguments to pass
  Bool isDynamic      // true if this is a -> dynamic call
  Bool isCtorChain    // true if this is MethodDef.ctorChain call
  Bool noParens       // was this call accessed without parens
  Bool isCallOp       // was this 'target()' (instead of 'target.name()')
  Bool isItAdd        // if using comma operator
  CMethod? method     // resolved method
  override Bool synthetic := false
}

**************************************************************************
** ShortcutExpr
**************************************************************************

**
** ShortcutExpr is used for operator expressions which are a shortcut
** to a method call:
**   a + b     =>  a.plus(b)
**   a - b     =>  a.minus(b)
**   a * b     =>  a.mult(b)
**   a / b     =>  a.div(b)
**   a % b     =>  a.mod(b)
**   a[b]      =>  a.get(b)
**   a[b] = c  =>  a.set(b, c)
**   -a        =>  a.negate()
**   ++a, a++  =>  a.increment()
**   --a, a--  =>  a.decrement()
**   a == b    =>  a.equals(b)
**   a != b    =>  ! a.equals(b)
**   a <=>     =>  a.compare(b)
**   a > b     =>  a.compare(b) > 0
**   a >= b    =>  a.compare(b) >= 0
**   a < b     =>  a.compare(b) < 0
**   a <= b    =>  a.compare(b) <= 0
**
class ShortcutExpr : CallExpr
{
  new makeUnary(Loc loc, Token opToken, Expr operand)
    : super.make(loc, null, null, ExprId.shortcut)
  {
    this.op      = opToken.toShortcutOp(1)
    this.opToken = opToken
    this.name    = op.methodName
    this.target  = operand
  }

  new makeBinary(Expr lhs, Token opToken, Expr rhs)
    : super.make(lhs.loc, null, null, ExprId.shortcut)
  {
    this.op      = opToken.toShortcutOp(2)
    this.opToken = opToken
    this.name    = op.methodName
    this.target  = lhs
    this.args.add(rhs)
  }

  new makeGet(Loc loc, Expr target, Expr index)
    : super.make(loc, null, null, ExprId.shortcut)
  {
    this.op      = ShortcutOp.get
    this.opToken = Token.lbracket
    this.name    = op.methodName
    this.target  = target
    this.args.add(index)
  }

  new makeFrom(ShortcutExpr from)
    : super.make(from.loc, null, null, ExprId.shortcut)
  {
    this.op      = from.op
    this.opToken = from.opToken
    this.name    = from.name
    this.target  = from.target
    this.args    = from.args
    this.isPostfixLeave = from.isPostfixLeave
  }

  override Bool assignRequiresTempVar() { isAssignable }

  override Obj? assignTarget() { isAssign ? target : null }

  override Bool isAssignable() { op === ShortcutOp.get }

  override Bool isCompare() { op === ShortcutOp.eq || op === ShortcutOp.cmp }

  override Bool isStmt() { isAssign || op === ShortcutOp.set }

  Bool isAssign() { opToken.isAssign || opToken.isIncrementOrDecrement }

  Bool isStrConcat() { opToken == Token.plus && args.size == 1 && target.ctype.isStr }

  override Str toStr()
  {
    if (op == ShortcutOp.get) return "${target}[$args.first]"
    if (op == ShortcutOp.increment) return isPostfixLeave ? "${target}++" : "++${target}"
    if (op == ShortcutOp.decrement) return isPostfixLeave ? "${target}--" : "--${target}"
    if (isAssign) return "${target} ${opToken} ${args.first}"
    if (op.degree == 1) return "${opToken}${target}"
    if (op.degree == 2) return "(${target} ${opToken} ${args.first})"
    return super.toStr
  }

  override Void print(AstWriter out)
  {
    out.w(toStr())
  }

  ShortcutOp op
  Token opToken
  Bool isPostfixLeave := false  // x++ or x-- (must have Expr.leave set too)
  MethodVar? tempVar    // temp local var to store += to field/indexed
}

**
** IndexedAssignExpr is a subclass of ShortcutExpr used
** in situations like x[y] += z where we need keep of two
** extra scratch variables and the get's matching set method.
** Note this class models the top x[y] += z, NOT the get target
** which is x[y].
**
** In this example, IndexedAssignExpr shortcuts Int.plus and
** its target shortcuts List.get:
**   x := [2]
**   x[0] += 3
**
class IndexedAssignExpr : ShortcutExpr
{
  new makeFrom(ShortcutExpr from)
    : super.makeFrom(from)
  {
  }

  MethodVar? scratchA
  MethodVar? scratchB
  CMethod? setMethod
}

**************************************************************************
** FieldExpr
**************************************************************************

**
** FieldExpr is used for a field variable access.
**
class FieldExpr : NameExpr
{
  new make(Loc loc, Expr? target := null, CField? field := null, Bool useAccessor := true)
    : super(loc, ExprId.field, target, null)
  {
    this.useAccessor = useAccessor
    this.isSafe = false
    if (field != null)
    {
      this.name  = field.name
      this.field = field
      this.ctype = field.fieldType
    }
  }

  override Bool isAssignable() { true }

  override Bool assignRequiresTempVar() { !field.isStatic }

  override Bool sameVarAs(Expr that)
  {
    x := that as FieldExpr
    if (x == null) return false
    return field == x.field &&
           target != null &&
           x.target != null &&
           target.sameVarAs(x.target)
  }

  override Int? asTableSwitchCase()
  {
    // TODO - this should probably be tightened up if we switch to const
    if (field.isStatic && field.parent.isEnum && ctype.isEnum)
    {
      switch (field.typeof)
      {
        case ReflectField#:
          ifield := field as ReflectField
          return ((Enum)ifield.f.get).ordinal
        case FieldDef#:
          fieldDef := field as FieldDef
          enumDef := fieldDef.parentDef.enumDef(field.name)
          if (enumDef != null) return enumDef.ordinal
        case FField#:
          ffield := field as FField
          attr := ffield.attr(FConst.EnumOrdinalAttr)
          if (attr != null) return attr.u2
        default:
          throw Err("Invalid field for tableswitch: $field.typeof $loc.toLocStr")
      }
    }
    return null
  }

  override Str serialize()
  {
    if (field.isStatic)
    {
      if (field.parent.isFloat)
      {
        switch (name)
        {
          case "nan":    return "sys::Float(\"NaN\")"
          case "posInf": return "sys::Float(\"INF\")"
          case "negInf": return "sys::Float(\"-INF\")"
        }
      }

      if (field.isEnum)
        return "${field.parent.qname}(\"$name\")"
    }

    return super.serialize
  }

  override Str toStr()
  {
    s := StrBuf.make
    if (target != null) s.add(target).add(".");
    if (!useAccessor) s.add("@")
    s.add(name)
    return s.toStr
  }

  CField? field       // resolved field
  Bool useAccessor    // false if access using '*' storage operator
}

**************************************************************************
** LocalVarExpr
**************************************************************************

**
** LocalVarExpr is used to access a local variable stored in a register.
**
class LocalVarExpr : Expr
{
  new make(Loc loc, MethodVar? var, ExprId id := ExprId.localVar)
    : super(loc, id)
  {
    if (var != null)
    {
      this.var = var
      this.ctype = var.ctype
    }
  }

  static LocalVarExpr makeNoUnwrap(Loc loc, MethodVar var)
  {
    self := make(loc, var, ExprId.localVar)
    self.unwrap = false
    return self
  }

  override Bool isAssignable() { true }

  override Bool assignRequiresTempVar() { var.usedInClosure }

  override Bool sameVarAs(Expr that)
  {
    x := that as LocalVarExpr
    if (x == null) return false
    if (var?.usedInClosure != x?.var?.usedInClosure) return false
    return register == x.register
  }

  virtual Int register() { var.register }

  override Str toStr()
  {
    if (var == null) return "???"
    return var.name
  }

  MethodVar? var        // bound variable
  Bool unwrap := true   // if hoisted onto heap with wrapper
}

**************************************************************************
** ThisExpr
**************************************************************************

**
** ThisExpr models the "this" keyword to access the implicit this
** local variable always stored in register zero.
**
class ThisExpr : LocalVarExpr
{
  new make(Loc loc, CType? ctype := null)
    : super(loc, null, ExprId.thisExpr)
  {
    this.ctype = ctype
  }

  override Bool isAssignable() { false }

  override Int register() { 0 }

  override Str toStr() { "this" }
}

**************************************************************************
** SuperExpr
**************************************************************************

**
** SuperExpr is used to access super class slots.  It always references
** the implicit this local variable stored in register zero, but the
** super class's slot definitions.
**
class SuperExpr : LocalVarExpr
{
  new make(Loc loc, CType? explicitType := null)
    : super(loc, null, ExprId.superExpr)
  {
    this.explicitType = explicitType
  }

  override Bool isAssignable() { false }

  override Int register() { 0 }

  override Str toStr()
  {
    if (explicitType != null)
      return "${explicitType}.super"
    else
      return "super"
  }

  CType? explicitType   // if "named super"
}

**************************************************************************
** ItExpr
**************************************************************************

**
** ItExpr models the "it" keyword to access the implicit
** target of an it-block.
**
class ItExpr : LocalVarExpr
{
  new make(Loc loc, CType? ctype := null)
    : super(loc, null, ExprId.itExpr)
  {
    this.ctype = ctype
  }

  override Bool isAssignable() { false }

  override Int register() { 1 }  // Void doCall(Type it)

  override Str toStr() { "it" }
}

**************************************************************************
** StaticTargetExpr
**************************************************************************

**
** StaticTargetExpr wraps a type reference as an Expr for use as
** a target in a static field access or method call
**
class StaticTargetExpr : Expr
{
  new make(Loc loc, CType ctype)
    : super(loc, ExprId.staticTarget)
  {
    this.ctype = ctype
  }

  override Bool sameVarAs(Expr that)
  {
    that.id === ExprId.staticTarget && ctype == that.ctype
  }

  override Str toStr()
  {
    return ctype.signature
  }
}

**************************************************************************
** TypeCheckExpr
**************************************************************************

**
** TypeCheckExpr is an expression which is composed of an arbitrary
** expression and a type - is, as, coerce
**
class TypeCheckExpr : Expr
{
  new make(Loc loc, ExprId id, Expr target, CType check)
    : super(loc, id)
  {
    this.target = target
    this.check  = check
    this.ctype  = check
  }

  new coerce(Expr target, CType to)
    : super.make(target.loc, ExprId.coerce)
  {
    if (to.isGenericParameter) to = to.ns.objType // TODO: not sure about this
    this.target = target
    this.from   = target.ctype
    this.check  = to
    this.ctype  = to
    this.synthetic = true
  }

  override Void walkChildren(Visitor v)
  {
    target = walkExpr(v, target)
  }

  override Bool isStmt()
  {
    return id === ExprId.coerce && target.isStmt
  }

  override Bool isAlwaysNullable() { id === ExprId.asExpr }

  override Bool isDefiniteAssign(|Expr lhs->Bool| f) { target.isDefiniteAssign(f) }

  override Str serialize()
  {
    if (id == ExprId.coerce)
      return target.serialize
    else
      return super.serialize
  }

  Str opStr()
  {
    switch (id)
    {
      case ExprId.isExpr:    return "is"
      case ExprId.isnotExpr: return "isnot"
      case ExprId.asExpr:    return "as"
      default:               throw Err(id.toStr)
    }
  }

  override Str toStr()
  {
    switch (id)
    {
      case ExprId.isExpr:    return "($target is $check)"
      case ExprId.isnotExpr: return "($target isnot $check)"
      case ExprId.asExpr:    return "($target as $check)"
      case ExprId.coerce:    return "(($check)$target)"
      default:               throw Err(id.toStr)
    }
  }

  ** From type if coerce
  CType? from { get { &from ?: target.ctype } }

  Expr target
  CType check    // to type if coerce
  override Bool synthetic := false
}

**************************************************************************
** TernaryExpr
**************************************************************************

**
** TernaryExpr is used for the ternary expression <cond> ? <true> : <false>
**
class TernaryExpr : Expr
{
  new make(Expr condition, Expr trueExpr, Expr falseExpr)
    : super(condition.loc, ExprId.ternary)
  {
    this.condition = condition
    this.trueExpr  = trueExpr
    this.falseExpr = falseExpr
  }

  override Void walkChildren(Visitor v)
  {
    condition = condition.walk(v)
    trueExpr  = trueExpr.walk(v)
    falseExpr = falseExpr.walk(v)
  }

  override Str toStr()
  {
    return "$condition ? $trueExpr : $falseExpr"
  }

  Expr condition     // boolean test
  Expr trueExpr      // result of expression if condition is true
  Expr falseExpr     // result of expression if condition is false
}

**************************************************************************
** ComplexLiteral
**************************************************************************

**
** ComplexLiteral is used to model a serialized complex object
** declared in facets.  It is only used in facets, in all other
** code complex literals are parsed as it-block ClosureExprs.
**
class ComplexLiteral : Expr
{
  new make(Loc loc, CType ctype)
    : super(loc, ExprId.complexLiteral)
  {
    this.ctype = ctype
    this.names = Str[,]
    this.vals  = Expr[,]
  }

  override Void walkChildren(Visitor v)
  {
    vals = walkExprs(v, vals)
  }

  override Str toStr() { doToStr |expr| { expr.toStr } }

  override Str serialize() { doToStr |expr| { expr.serialize } }

  Str doToStr(|Expr->Str| f)
  {
    s := StrBuf()
    s.add("$ctype {")
    names.each |Str n, Int i| { s.add("$n = ${f(vals[i])};") }
    s.add("}")
    return s.toStr
  }

  Str[] names
  Expr[] vals
}

**************************************************************************
** ClosureExpr
**************************************************************************

**
** ClosureExpr is an "inlined anonymous method" which closes over it's
** lexical scope.  ClosureExpr is placed into the AST by the parser
** with the code field containing the method implementation.  In
** InitClosures we remap a ClosureExpr to an anonymous class TypeDef
** which extends Func.  The function implementation is moved to the
** anonymous class's doCall() method.  However we leave ClosureExpr
** in the AST in it's original location with a substitute expression.
** The substitute expr just creates an instance of the anonymous class.
** But by leaving the ClosureExpr in the tree, we can keep track of
** the original lexical scope of the closure.
**
class ClosureExpr : Expr
{
  new make(Loc loc, TypeDef enclosingType,
           SlotDef enclosingSlot, ClosureExpr? enclosingClosure,
           FuncType signature, Str name)
    : super(loc, ExprId.closure)
  {
    this.ctype            = signature
    this.enclosingType    = enclosingType
    this.enclosingSlot    = enclosingSlot
    this.enclosingClosure = enclosingClosure
    this.signature        = signature
    this.name             = name
  }

  once CField outerThisField()
  {
    if (enclosingSlot.isStatic) throw Err("Internal error: $loc.toLocStr")
    return ClosureVars.makeOuterThisField(this)
  }

  override Str toStr()
  {
    return "$signature { ... }"
  }

  override Void print(AstWriter out)
  {
    out.w(signature.toStr)
    if (substitute != null)
    {
      out.w(" { substitute: ")
      substitute.print(out)
      out.w(" }").nl
    }
    else
    {
      out.nl
      code.print(out)
    }
  }

  override Bool isDefiniteAssign(|Expr lhs->Bool| f)
  {
    // at this point, we have moved code into doCall method
    if (doCall == null) return false
    return doCall.code.isDefiniteAssign(f)
  }

  Expr toWith(Expr target)
  {
    if (target.ctype != null) setInferredSignature(FuncType.makeItBlock(target.ctype))
    x := CallExpr.makeWithMethod(loc, target, enclosingType.ns.objWith, Expr[this])
    // TODO: this coercion should be added automatically later in the pipeline
    if (target.ctype == null) return x
    return TypeCheckExpr.coerce(x, target.ctype)
  }

  Void setInferredSignature(FuncType t)
  {
    // bail if we didn't expect an inferred the signature
    // or haven't gotten to InitClosures yet
    if (!signature.inferredSignature || cls == null) return

    // between the explicit signature and the inferred
    // signature, take the most specific types; this is where
    // we take care of functions with generic parameters like V
    t = t.toArity(((FuncType)cls.base).arity)
    t = signature.mostSpecific(t)

    // sanity check
    if (t.usesThis)
      throw Err("Inferring signature with un-parameterized this type: $t")

    // update my signature and the doCall signature
    signature = t
    ctype = t
    if (doCall != null)
    {
      // update parameter types
      doCall.paramDefs.each |ParamDef p, Int i|
      {
        if (i < signature.params.size)
          p.paramType = signature.params[i]
      }

      // update return, we might have to translate an single
      // expression statement into a return statement
      if (doCall.ret.isVoid && !t.ret.isVoid)
      {
        doCall.ret = t.ret
        collapseExprAndReturn(doCall)
        collapseExprAndReturn(call)
      }
    }

    // if an itBlock, set type of it
    if (isItBlock) itType = t.params.first

    // update base type of Func subclass
    cls.base = t
  }

  Void collapseExprAndReturn(MethodDef m)
  {
    code := m.code.stmts
    if (code.size != 2) return
    if (code[0].id !== StmtId.expr) return
    if (code[1].id !== StmtId.returnStmt) return
    if (!((ReturnStmt)code.last).isSynthetic) return
    expr := ((ExprStmt)code.first).expr
    code.set(0, ReturnStmt.makeSynthetic(expr.loc, expr))
    code.removeAt(1)
  }

  // Parse
  TypeDef enclosingType         // enclosing class
  SlotDef enclosingSlot         // enclosing method or field initializer
  ClosureExpr? enclosingClosure // if nested closure
  FuncType signature            // function signature
  Block? code                   // moved into a MethodDef in InitClosures
  Str name                      // anonymous class name
  Bool isItBlock                // does closure have implicit it scope

  // InitClosures
  CallExpr? substitute          // expression to substitute during assembly
  TypeDef? cls                  // anonymous class which implements the closure
  MethodDef? call               // anonymous class's call() with code
  MethodDef? doCall             // anonymous class's doCall() with code

  // ResolveExpr
  [Str:MethodVar]? enclosingVars // my parent methods vars in scope
  Bool setsConst                 // sets one or more const fields (CheckErrors)
  CType? itType                  // type of implicit it
}

**************************************************************************
** ClosureExpr
**************************************************************************

**
** DslExpr is an embedded Domain Specific Language which
** is parsed by a DslPlugin.
**
class DslExpr : Expr
{
  new make(Loc loc, CType anchorType, Loc srcLoc, Str src)
    : super(loc, ExprId.dsl)
  {
    this.anchorType = anchorType
    this.src        = src
    this.srcLoc     = srcLoc
  }

  override Str toStr()
  {
    return "$anchorType <|$src|>"
  }

  override Void print(AstWriter out)
  {
    out.w(toStr)
  }

  CType anchorType  // anchorType <|src|>
  Str src           // anchorType <|src|>
  Loc srcLoc        // location of first char of src
  Int leadingTabs   // number of leading tabs on original Fantom line
  Int leadingSpaces // number of leading non-tab chars on original Fantom line
}

**************************************************************************
** ThrowExpr
**************************************************************************

**
** ThrowExpr models throw as an expr versus a statement
** for use inside ternary/elvis operations.
**
class ThrowExpr : Expr
{
  new make(Loc loc, Expr exception)
    : super(loc, ExprId.throwExpr)
  {
    this.exception = exception
  }

  override Void walkChildren(Visitor v)
  {
    exception = exception.walk(v)
  }

  override Str toStr() { "throw $exception" }

  Expr exception   // exception to throw
}

**************************************************************************
** ExprId
**************************************************************************

**
** ExprId uniquely identifies the type of expr
**
enum class ExprId
{
  nullLiteral,      // LiteralExpr
  trueLiteral,
  falseLiteral,
  intLiteral,
  floatLiteral,
  decimalLiteral,
  strLiteral,
  durationLiteral,
  uriLiteral,
  typeLiteral,
  localeLiteral,    // LocaleLiteralExpr
  slotLiteral,      // SlotLiteralExpr
  rangeLiteral,     // RangeLiteralExpr
  listLiteral,      // ListLiteralExpr
  mapLiteral,       // MapLiteralExpr
  boolNot,          // UnaryExpr
  cmpNull,
  cmpNotNull,
  elvis,
  assign,           // BinaryExpr
  same,
  notSame,
  boolOr,           // CondExpr
  boolAnd,
  isExpr,           // TypeCheckExpr
  isnotExpr,
  asExpr,
  coerce,
  call,             // CallExpr
  construction,
  shortcut,         // ShortcutExpr (has ShortcutOp)
  field,            // FieldExpr
  localVar,         // LocalVarExpr
  thisExpr,         // ThisExpr
  superExpr,        // SuperExpr
  itExpr,           // ItExpr
  staticTarget,     // StaticTargetExpr
  unknownVar,       // UnknownVarExpr
  storage,
  ternary,          // TernaryExpr
  complexLiteral,   // ComplexLiteral
  closure,          // ClosureExpr
  dsl,              // DslExpr
  throwExpr         // ThrowExpr
}

**************************************************************************
** ShortcutId
**************************************************************************

**
** ShortcutOp is a sub-id for ExprId.shortcut which identifies the
** an shortuct operation and it's method call
**
enum class ShortcutOp
{
  plus(2, "+"),
  minus(2, "-"),
  mult(2, "*"),
  div(2, "/"),
  mod(2, "%"),
  negate(1, "-"),
  increment(1, "++"),
  decrement(1, "--"),
  eq(2, "==", "equals"),
  cmp(2, "<=>", "compare"),
  get(2, "[]"),
  set(3, "[]="),
  add(2, ",")

  private new make(Int degree, Str symbol, Str? methodName := null)
  {
    this.degree = degree
    this.symbol = symbol
    this.methodName = methodName == null ? name : methodName
    this.isOperator = methodName == null
  }

  static ShortcutOp? fromPrefix(Str prefix) { prefixes[prefix] }
  private static const Str:ShortcutOp prefixes
  static
  {
    m := Str:ShortcutOp[:]
    vals.each |val| { m[val.methodName] = val }
    prefixes = m
  }

  Str formatErr(CType lhs, CType rhs)
  {
    if (this === get) return "$lhs [ $rhs ]"
    if (this === set) return "$lhs [ $rhs ]="
    return "$lhs $symbol $rhs"
  }

  const Int degree
  const Str methodName
  const Bool isOperator
  const Str symbol
}