Fan

 

class

compilerJs::JsBlock

sys::Obj
  compilerJs::JsNode
    compilerJs::JsBlock
//
// Copyright (c) 2009, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   9 Jul 09  Andy Frank  Creation
//

using compiler

**
** JsBlock
**
** TODO FIXIT - TEMP TILL WE REFACTOR THIS CODE!!!!
**
class JsBlock : JsNode
{
  new make(Node n)
  {
    this.x = n
  }

  override Void write(JsWriter out)
  {
    this.out = out
    block(x)
  }

//////////////////////////////////////////////////////////////////////////
// Block
//////////////////////////////////////////////////////////////////////////

  Void block(Block block, Bool braces := false, Bool indent := true)
  {
    if (braces) out.w("{").nl
    if (indent) out.indent
    block.stmts.each |Stmt s| { stmt(s) }
    if (indent) out.unindent
    if (braces) out.w("}").nl
  }

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

  Void stmt(Stmt stmt, Bool nl := true)
  {
    switch (stmt.id)
    {
      case StmtId.nop:          return
      case StmtId.expr:         exprStmt(stmt->expr)
      case StmtId.localDef:     localDef(stmt); if (nl) out.nl
      case StmtId.ifStmt:       ifStmt(stmt)
      case StmtId.returnStmt:   returnStmt(stmt); if (nl) out.nl
      case StmtId.throwStmt:    throwStmt(stmt); if(nl) out.nl
      case StmtId.forStmt:      forStmt(stmt)
      case StmtId.whileStmt:    whileStmt(stmt)
      case StmtId.breakStmt:    out.w("break;"); if (nl) out.nl
      case StmtId.continueStmt: out.w("continue;"); if (nl) out.nl
      case StmtId.tryStmt:      tryStmt(stmt)
      case StmtId.switchStmt:   switchStmt(stmt)
      default: throw CompilerErr("Unknown StmtId: $stmt.id", stmt.location)
    }
  }

  Void exprStmt(Expr ex)
  {
    if (!ex.toStr.startsWith("(\$cvars ="))
    {
      expr(ex)
      out.w(";").nl
    }
  }

  Void localDef(LocalDefStmt lds)
  {
    out.w("var ")
    if (lds.init == null) out.w(lds.name)
    else expr(lds.init)
    out.w(";")
  }

  Void returnStmt(ReturnStmt rs)
  {
// TODO
//    if (inStaticInit) return
    out.w("return")
    if (rs.expr != null) { out.w(" "); expr(rs.expr) }
    out.w(";")
  }

  Void throwStmt(ThrowStmt ts)
  {
    out.w("throw ")
    expr(ts.exception)
    out.w(";")
  }

  Void ifStmt(IfStmt fs)
  {
    out.w("if ("); expr(fs.condition); out.w(")").nl
    block(fs.trueBlock, true)
    if (fs.falseBlock != null)
    {
      out.w("else").nl
      block(fs.falseBlock, true)
    }
  }

  Void forStmt(ForStmt fs)
  {
    out.w("for (")
    if (fs.init != null) { stmt(fs.init, false); out.w(" ") }
    else out.w("; ")
    if (fs.condition != null) expr(fs.condition)
    out.w("; ")
    if (fs.update != null) expr(fs.update)
    out.w(")").nl
    block(fs.block, true)
  }

  Void whileStmt(WhileStmt ws)
  {
    out.w("while (")
    expr(ws.condition)
    out.w(")").nl
    block(ws.block, true)
  }

  Void tryStmt(TryStmt ts)
  {
    out.w("try").nl
    block(ts.block, true)
    // TODO
    //ts.catches.each |Catch c|
c := ts.catches.first
if (c != null)
    {
      errVar := c.errVariable ?: "err"
      out.w("catch ($errVar)").nl
      out.w("{").nl
      out.w("  $errVar = fan.sys.Err.make($errVar);").nl
      block(c.block, false)
      out.w("}").nl
    }
    if (ts.catches.size == 0 && ts.finallyBlock == null)
    {
      // TODO - is this right?
      out.w("catch (err) {}").nl
    }
    if (ts.finallyBlock != null)
    {
      out.w("finally").nl
      block(ts.finallyBlock, true)
    }
  }

  Void switchStmt(SwitchStmt ss)
  {
    var := unique
    out.w("var $var = "); expr(ss.condition); out.w(";").nl
    ss.cases.each |c,ia|
    {
      if (ia > 0) out.w("else ")
      out.w("if (")
      c.cases.each |e,ib|
      {
        if (ib > 0) out.w(" || ")
        out.w("fan.sys.Obj.equals($var,"); expr(e); out.w(")")
      }
      out.w(")").nl
      if (c.block != null) block(c.block, true)
      else out.w("{").nl.w("}").nl  // TODO - is this right??
    }
    if (ss.defaultBlock != null)
    {
      out.w("else").nl
      block(ss.defaultBlock, true)
    }
  }

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

  Void expr(Expr ex)
  {
    switch (ex.id)
    {
      case ExprId.nullLiteral:  out.w("null")
      case ExprId.trueLiteral:  out.w("true")
      case ExprId.falseLiteral: out.w("false")
      case ExprId.intLiteral:   intLiteralExpr(ex)
      case ExprId.floatLiteral: out.w("fan.sys.Float.make($ex)")
      case ExprId.decimalLiteral: out.w("fan.sys.Decimal.make($ex)")
      case ExprId.strLiteral:   out.w("\"").w(ex->val.toStr.toCode('\"', true)[1..-2]).w("\"")
      case ExprId.durationLiteral: out.w("fan.sys.Duration.fromStr(\"").w(ex).w("\")")
      case ExprId.uriLiteral:   out.w("fan.sys.Uri.fromStr(").w(ex->val.toStr.toCode('\"', true)).w(")")
      case ExprId.typeLiteral:  out.w("fan.sys.Type.find(\"${ex->val->signature}\")")
      case ExprId.slotLiteral:  out.w("fan.sys.Type.find(\"${ex->parent->signature}\").slot(\"${ex->name}\")")
      case ExprId.rangeLiteral: rangeLiteralExpr(ex)
      case ExprId.listLiteral:  listLiteralExpr(ex)
      case ExprId.mapLiteral:   mapLiteralExpr(ex)
      case ExprId.boolNot:      out.w("!"); expr(ex->operand)
      case ExprId.cmpNull:      expr(ex->operand); out.w(" == null")
      case ExprId.cmpNotNull:   expr(ex->operand); out.w(" != null")
      case ExprId.elvis:        elvisExpr(ex)
      case ExprId.assign:       assignExpr(ex)
      case ExprId.same:         expr(ex->lhs); out.w(" === "); expr(ex->rhs)
      case ExprId.notSame:      out.w("!("); expr(ex->lhs); out.w(" === "); expr(ex->rhs); out.w(")")
      case ExprId.boolOr:       condExpr(ex)
      case ExprId.boolAnd:      condExpr(ex)
      case ExprId.isExpr:       typeCheckExpr(ex)
      case ExprId.isnotExpr:    out.w("!"); typeCheckExpr(ex)
      case ExprId.asExpr:       typeCheckExpr(ex)
      case ExprId.coerce:       expr(ex->target)
      case ExprId.call:         callExpr(ex)
      case ExprId.construction: callExpr(ex)
      case ExprId.shortcut:     shortcutExpr(ex)
      case ExprId.field:        fieldExpr(ex)
      case ExprId.localVar:     out.w(vnameToJs(ex.toStr))
      case ExprId.thisExpr:     out.w(inClosure ? "\$this" : "this")
      case ExprId.superExpr:    out.w("this.\$super")
      case ExprId.itExpr:       out.w("it")
      case ExprId.staticTarget: out.w(qnameToJs(ex->ctype))
      //case ExprId.unknownVar
      //case ExprId.storage
      case ExprId.ternary:      expr(ex->condition); out.w(" ? "); expr(ex->trueExpr); out.w(" : "); expr(ex->falseExpr)
      //case ExprId.curry
      //case ExprId.complexLiteral
      case ExprId.closure:      closureExpr(ex)
      default: throw CompilerErr("Unknown ExprId: $ex.id", ex.location)
    }
  }

  Void intLiteralExpr(LiteralExpr le)
  {
    val := le.val as Int
    if (val.abs > maxInt || val == Int.minVal)
    {
      hi := (val >> 32) & 0xffff_ffff
      lo := val & 0xffff_ffff
      out.w("new Long(0x$hi.toHex,0x$lo.toHex)")
    }
    else out.w(val)
  }

  Void rangeLiteralExpr(RangeLiteralExpr re)
  {
    out.w("fan.sys.Range.make(")
    expr(re.start)
    out.w(",")
    expr(re.end)
    if (re.exclusive) out.w(",true")
    out.w(")")
  }

  Void listLiteralExpr(ListLiteralExpr le)
  {
    t := le.explicitType != null ? le.explicitType.v.qname : Obj#.qname
    out.w("fan.sys.List.make(fan.sys.Type.find(\"$t\"), [")
    le.vals.each |Expr ex, Int i|
    {
      if (i > 0) out.w(",")
      expr(ex)
    }
    out.w("])")
  }

  Void mapLiteralExpr(MapLiteralExpr me)
  {
    out.w("fan.sys.Map.fromLiteral([")
    me.keys.each |Expr key, Int i| { if (i > 0) out.w(","); expr(key) }
    out.w("],[")
    me.vals.each |Expr val, Int i| { if (i > 0) out.w(","); expr(val) }
    out.w("]")
    if (me.explicitType != null)
    {
      out.w(",fan.sys.Type.find(\"").w(me.explicitType.k).w("\")")
      out.w(",fan.sys.Type.find(\"").w(me.explicitType.v).w("\")")
    }
    out.w(")")
  }

  Void elvisExpr(BinaryExpr be)
  {
    out.w("("); expr(be.lhs); out.w(" != null)")
    out.w(" ? ("); expr(be.lhs); out.w(")")
    out.w(" : ("); expr(be.rhs); out.w(")")
  }

  Void assignExpr(BinaryExpr be)
  {
    if (be.lhs is FieldExpr)
    {
      fe := be.lhs as FieldExpr
      if (fe.useAccessor) { fieldExpr(fe,false); out.w("("); expr(be.rhs); out.w(")") }
      else { fieldExpr(fe); out.w(" = "); expr(be.rhs); }
    }
    else { expr(be.lhs); out.w(" = "); expr(be.rhs) }
  }

  Void typeCheckExpr(TypeCheckExpr te)
  {
    method := te.id == ExprId.asExpr ? "as" : "is"
    out.w("fan.sys.Obj.$method(")
    expr(te.target)
    out.w(",fan.sys.Type.find(\"$te.check\"))")
  }

  Void callExpr(CallExpr ce, Bool doSafe := true)
  {
    // skip mock methods used to insert implicit runtime checks
    if (ce.method is MockMethod) return

    if (ce.isSafe && doSafe)
    {
      out.w("((")
      expr(ce.target)
      out.w(" == null) ? null : (")
      callExpr(ce, false)
      out.w("))")
      return
    }

    // check for special cases
    if (isObjMethod(ce.method.name))
    {
      firstArg := true
      if (ce is ShortcutExpr && ce->opToken.toStr == "!=") out.w("!")
      if (ce.target is SuperExpr)
      {
        base := ce.target->explicitType ?: ce.target.ctype
        out.w(qnameToJs(base)).w(".prototype.${ce.method.name}.call(this,")
        firstArg = false
      }
      else
      {
        out.w("fan.sys.Obj.${vnameToJs(ce.method.name)}(")
        expr(ce.target)
      }
      ce.args.each |arg, i|
      {
        if (i>0 || firstArg) out.w(", ")
        expr(arg)
      }
      out.w(")")
      if (ce is ShortcutExpr && ce->op === ShortcutOp.cmp && ce->opToken.toStr != "<=>")
        out.w(" ${ce->opToken} 0")
      return
    }

    // normal case
    if (ce.target != null)
    {
      if (isPrimitive(ce.target.ctype.toStr) ||
          ce.target.ctype.isList ||
          ce.target.ctype.isFunc ||
          ce.target is TypeCheckExpr ||
          ce.target is ItExpr)
      {
        ctype := ce.target.ctype
        route := false
        if (ce.target is TypeCheckExpr) ctype = ce.target->check
        if (ctype.isList)      { out.w("fan.sys.List.${vnameToJs(ce.name)}("); route=true }
        else if (ctype.isFunc) { out.w("fan.sys.Func.${vnameToJs(ce.name)}("); route=true }
        else if (isPrimitive(ctype.toStr))
        {
          mname := ce.name
          if (ce.method.isCtor || mname == "<ctor>")
          {
            if (mname == "<ctor>") mname = "make"
            first := ce.method.params.first
            if (ce.args.size == 1 && first?.paramType?.qname == "sys::Str")
              mname = "fromStr"
          }
          out.w("${qnameToJs(ctype)}.${vnameToJs(mname)}(")
          route = true
        }
        i := 0
        if (!route)
        {
          expr(ce.target)
          out.w(".${vnameToJs(ce.name)}(")
        }
        else if (!ce.method.isStatic)
        {
          expr(ce.target)
          if (ce.args.size > 0) i++
        }
        ce.args.each |arg| { if (i++ > 0) out.w(","); expr(arg) }
        out.w(")")
        return
      }
      else if (ce.target.ctype.qname == "sys::Err" && ce.method.name == "trace")
      {
        out.w("fan.sys.Err.trace(")
        expr(ce.target)
        out.w(")")
        return
      }
      if (ce.target is SuperExpr)
      {
        base := ce.target->explicitType ?: ce.target.ctype
        out.w(qnameToJs(base)).w(".prototype")
      }
      else
      {
        expr(ce.target)
      }
    }
    else if (ce.method.isStatic || ce.method.isCtor)
    {
      out.w(qnameToJs(ce.method.parent))
    }
    Str? mname := vnameToJs(ce.name)
    if (ce.method.isCtor || ce.name == "<ctor>")
    {
      mname = ce.name == "<ctor>" ? "make" : ce.name
      if (ce.target is SuperExpr) mname = "$mname\$"
      first := ce.method.params.first
      if (ce.args.size == 1 && first?.paramType?.qname == "sys::Str")
      {
        parent  := ce.method.parent
        fromStr := parent.methods.find |m| { m.parent.qname == parent.qname && m.name == "fromStr" }
        if (fromStr != null) mname = "fromStr"
      }
    }
    else if (ce.target != null)
    {
      // TODO - not sure we need this, or if its right...
      if (ce.target.ctype.qname == "sys::Func" && Regex("call\\d").matches(mname))
        mname = null
    }
    if (mname != null) out.w(".").w(mname)
    if (ce.isDynamic && ce.noParens)
    {
      if (ce.args.size == 0) return
      if (ce.args.size == 1)
      {
        out.w(" = ")
        expr(ce.args.first)
        return
      }
      throw ArgErr("Parens required for multiple args")
    }
    i := 0
    if (ce.target is SuperExpr)
    {
      out.w(".call(this")
      i++
    }
    else out.w("(")
    ce.args.each |arg|
    {
      if (i++ > 0) out.w(", ")
      expr(arg)
    }
    out.w(")")
  }

  Void shortcutExpr(ShortcutExpr se)
  {
    // try to optimize the primitive case
    if (isPrimitive(se.target.ctype?.qname) &&
        se.method.name != "compare" && se.method.name != "get" && se.method.name != "slice")
    {
      lhs := se.target
      rhs := se.args.first
      if (se.op == ShortcutOp.increment)
      {
        if (se.isPostfixLeave) { expr(lhs); out.w("++") }
        else { out.w("++"); expr(lhs) }
        return
      }
      if (se.op == ShortcutOp.decrement)
      {
        if (se.isPostfixLeave) { expr(lhs); out.w("--") }
        else { out.w("--"); expr(lhs) }
        return
      }
      if (se.target.ctype?.qname == "sys::Int")
      {
        Str? op := null
        switch (se.op)
        {
          case ShortcutOp.plus:   op = "plus"
          case ShortcutOp.minus:  op = "minus"
          case ShortcutOp.mult:   op = "mult"
          case ShortcutOp.div:    op = "div"
          case ShortcutOp.mod:    op = "mod"
          case ShortcutOp.and:    op = "and"
          case ShortcutOp.or:     op = "or"
          case ShortcutOp.lshift: op = "shl"
          case ShortcutOp.rshift: op = "shr"
        }
        if (op != null)
        {
          if (se.isAssign) { expr(lhs); out.w(" = ") }
          if (se.opToken == Token.notEq) out.w("!")
          out.w("fan.sys.Int.$op(")
          expr(lhs)
          out.w(",")
          expr(rhs)
          out.w(")")
          return
        }
      }
      if (se.target.ctype?.qname == "sys::Float")
      {
        if (se.op == ShortcutOp.eq)
        {
          if (se.opToken == Token.notEq) out.w("!")
          out.w("fan.sys.Float.equals(")
          expr(lhs)
          out.w(",")
          expr(rhs)
          out.w(")")
          return
        }
      }
      if (se.op.degree == 1) { out.w(se.opToken); expr(lhs); return }
      if (se.op.degree == 2)
      {
        out.w("(")
        expr(lhs)
        out.w(" $se.opToken ")
        expr(rhs)
        out.w(")")
        return
      }
    }

    // check for list access
    if (!isPrimitive(se.target.ctype?.qname) &&
        se.op == ShortcutOp.get || se.op == ShortcutOp.set)
    {
      expr(se.target)
      if (!se.args.first.ctype.isInt)
      {
        out.w(se.args.size == 1 ? ".get" : ".set")
        out.w("(")
        expr(se.args.first)
        if (se.args.size > 1) { out.w(","); expr(se.args[1]) }
        out.w(")")
        return
      }
      i := "$se.args.first".toInt(10, false)
      if (i != null && i < 0)
      {
        out.w("[")
        expr(se.target)
        out.w(".length$i]")
      }
      else
      {
        out.w("[")
        expr(se.args.first)
        out.w("]")
      }
      if (se.args.size > 1)
      {
        out.w(" = ")
        expr(se.args[1])
      }
      return
    }

    // fallback to call as method
    callExpr(se)
  }

  Void condExpr(CondExpr ce)
  {
    ce.operands.each |Expr op, Int i|
    {
      if (i > 0 && i<ce.operands.size) out.w(" $ce.opToken ")
      expr(op)
    }
  }

  Void fieldExpr(FieldExpr fe, Bool get := true)
  {
    if (fe.target?.ctype?.isList == true)
    {
      if (fe.name == "size" && get)
      {
        expr(fe.target)
        out.w(".length")
      }
      else
      {
        out.w("fan.sys.List.${vnameToJs(fe.name)}(")
        expr(fe.target)
        out.w(get ? ")" : ",")
      }
      return
    }
    cvar := fe.target?.toStr == "\$cvars"
    name := fe.name
    if (fe.target != null && !cvar)
    {
      expr(fe.target)
      if (name == "\$this") return // skip $this ref for closures
      out.w(".")
    }
    if (cvar)
    {
      if (name[0] == '$') name = name[1..-1]
      else { i := name.index("\$"); if (i != null) name = name[0..<i] }
    }
    if (fe.target == null && fe.field.isStatic)
    {
      out.w(qnameToJs(fe.field.parent)).w(".")
    }
    out.w(vnameToJs(name))
    if (!cvar && fe.useAccessor) out.w(get ? "\$get()" : "\$set")
  }

  Void closureExpr(ClosureExpr ce)
  {
    closureLevel++
    out.w("fan.sys.Func.make(function(")
    ce.doCall.vars.each |MethodVar v, Int i|
    {
      if (!v.isParam) return
      if (i > 0) out.w(",")
      out.w(vnameToJs(v.name))
    }
    out.w(") {")
    if (ce.doCall?.code != null)
    {
      out.nl
      block(ce.doCall.code, false)
    }
    out.w("},[")
    ce.doCall.params.each |p,i|
    {
      if(i > 0) out.w(",")
      out.w("new fan.sys.Param(\"$p.name\",\"$p.paramType.qname\",$p.hasDefault)")
    }
    out.w("],fan.sys.Type.find(\"$ce.doCall.ret.qname\"))")
    closureLevel--
  }

//////////////////////////////////////////////////////////////////////////
// Util
//////////////////////////////////////////////////////////////////////////

  const Int maxInt := 9007199254740992  // max exact int in js (2^53)

  Int closureLevel := 0

  **
  ** Return true if we are inside a closure.
  **
  Bool inClosure()
  {
    return closureLevel > 0
  }

  Bool isPrimitive(Str qname) { return primitiveMap.get(qname, false) }
  const Str:Bool primitiveMap :=
  [
    "sys::Bool":     true,
    "sys::Bool?":    true,
    "sys::Decimal":  true,
    "sys::Decimal?": true,
    "sys::Float":    true,
    "sys::Float?":   true,
    "sys::Int":      true,
    "sys::Int?":     true,
    "sys::Num":      true,
    "sys::Num?":     true,
    "sys::Str":      true,
    "sys::Str?":     true,
  ]

  Bool isObjMethod(Str methodName) { return objMethodMap.get(methodName, false) }
  const Str:Bool objMethodMap :=
  [
    "equals":      true,
    "compare":     true,
    "isImmutable": true,
    "toStr":       true,
    "type":        true,
    "with":        true,
  ]

  Str unique() { return "\$_u${lastId++}" }
  Int lastId := 0

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

  Node x
  JsWriter? out

}