//
// Copyright (c) 2009, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//    9 Apr 09  Brian Frank  Creation
//   23 Mar 10  Brian Frank  FieldNotSetErr checks
//

**
** ConstChecks adds hooks into constructors and it-blocks
** to ensure that an attempt to set a const field will throw
** ConstErr if not in the objects constructor.  We also use
** this step to insert the runtime checks for non-nullable fields.
**
** For each it-block which sets const fields:
**
**   doCall(Foo it)
**   {
**     this.checkInCtor(it)
**     ...
**   }
**
** For each constructor which takes an it-block:
**
**   new make(..., |This| f)
**   {
**     f?.enterCtor(this)
**     ...
**     checksField$Foo()  // if non-nullable fields need runtime checks
**     f?.exitCtor()      // for every return
**     return
**   }
**
**
class ConstChecks : CompilerStep
{

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

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

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

  override Void run()
  {
    log.debug("ConstChecks")

    // walk all the closures
    compiler.closures.each |ClosureExpr c| { processClosure(c) }

    // walk all the types
    types.each |TypeDef t|
    {
      if (t.isNative) return
      this.curType = t

      // get all the fields which require runtime checks, and if there
      // are any, then generate a fieldCheck method to call on ctor exit
      this.fieldCheck = genFieldCheck(t)

      // walk all the constructors
      t.ctorDefs.each |MethodDef ctor| { processCtor(ctor) }
    }
  }

//////////////////////////////////////////////////////////////////////////
// Process Closure
//////////////////////////////////////////////////////////////////////////

  private Void processClosure(ClosureExpr c)
  {
    // don't process anything but it-blocks which use const fields
    if (!c.isItBlock || !c.setsConst) return

    // add inCtor check
    loc := c.loc
    check := CallExpr.makeWithMethod(loc, ThisExpr(loc), ns.funcCheckInCtor, [ItExpr(loc)])
    check.noLeave
    c.doCall.code.stmts.insert(0, check.toStmt)
  }

//////////////////////////////////////////////////////////////////////////
// Process Constructor
//////////////////////////////////////////////////////////////////////////

  private Void processCtor(MethodDef ctor)
  {
    // don't process static constructors
    if (ctor.isStatic) return

    // set current state
    this.curCtor = ctor

    // add func?.enterCtor(this)
    if (ctor.isItBlockCtor)
    {
      loc := ctor.loc
      enter := CallExpr.makeWithMethod(loc, LocalVarExpr(loc, itBlockVar), ns.funcEnterCtor, [ThisExpr(loc)])
      enter.isSafe = true
      enter.noLeave
      ctor.code.stmts.insert(0, enter.toStmt)
    }

    // walk all the statements and insert exitCtor before each return
    if (ctor.isItBlockCtor || fieldCheck != null)
      ctor.code.walk(this, VisitDepth.stmt)
  }

  override Stmt[]? visitStmt(Stmt stmt)
  {
    if (stmt.id !== StmtId.returnStmt) return null
    loc := stmt.loc
    result := Stmt[,]

    // insert call to func?.exitCtor()
    if (curCtor.isItBlockCtor)
    {
      exit1 := CallExpr.makeWithMethod(loc, LocalVarExpr(loc, itBlockVar), ns.funcExitCtor)
      exit1.isSafe = true
      exit1.noLeave
      result.add(exit1.toStmt)
    }

    // if needed insert call to this.fieldCheck()
    if (fieldCheck != null)
    {
      exit2 := CallExpr.makeWithMethod(loc, ThisExpr(loc), fieldCheck)
      exit2.noLeave
      result.add(exit2.toStmt)
    }

    return result.add(stmt)
  }

  private MethodVar itBlockVar() { curCtor.vars[curCtor.params.size-1] }

  private MethodDef? genFieldCheck(TypeDef t)
  {
    // find any fields which were marked as requiring a
    // runtime check during the CheckErrors step
    checkedFields := t.fieldDefs.findAll |f| { f.requiresNullCheck  }
    if (checkedFields.isEmpty) return null

    // add check for each field which requires runtime null check
    loc := t.loc
    block := Block(loc)
    checkedFields.each |FieldDef f|
    {
      // field == null
      condExpr := UnaryExpr(loc, ExprId.cmpNull, Token.same,
                            FieldExpr(loc, ThisExpr(loc), f, false))

      // throw FieldNotSet(f.qname)
      throwExpr := ThrowStmt(loc, CallExpr.makeWithMethod(loc, null, ns.fieldNotSetErrMake,
                             [LiteralExpr.makeStr(loc, ns, f.qname)]))

      // if (condExpr) throwExpr
      trueBlock := Block(loc)
      trueBlock.stmts.add(throwExpr)
      block.stmts.add(IfStmt(loc, condExpr, trueBlock))
    }
    block.stmts.add(ReturnStmt.makeSynthetic(loc))

    // create checkFields method
    m := MethodDef(loc, t)
    m.flags = FConst.Private.or(FConst.Synthetic)
    m.name  = "checkFields\$" + t.name
    m.ret   = ns.voidType
    m.code  = block
    t.addSlot(m)
    return m
  }

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

  MethodDef? curCtor
  MethodDef? fieldCheck
}