//
// Copyright (c) 2006, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 2 Dec 05 Brian Frank Creation
//
**
** Walk the AST to resolve:
** - Manage local variable scope
** - Resolve loop for breaks and continues
** - Resolve LocalDefStmt.init into full assignment expression
** - Resolve Expr.ctype
** - Resolve UknownVarExpr -> LocalVarExpr, FieldExpr, or CallExpr
** - Resolve CallExpr to their CMethod
**
class ResolveExpr : CompilerStep
{
//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////
new make(Compiler compiler)
: super(compiler)
{
}
//////////////////////////////////////////////////////////////////////////
// Run
//////////////////////////////////////////////////////////////////////////
override Void run()
{
log.debug("ResolveExpr")
walk(types, VisitDepth.expr)
bombIfErr
}
//////////////////////////////////////////////////////////////////////////
// Method
//////////////////////////////////////////////////////////////////////////
override Void enterMethodDef(MethodDef m)
{
super.enterMethodDef(m)
this.inClosure = (curType.isClosure && curType.closure.doCall === m)
initMethodVars
}
//////////////////////////////////////////////////////////////////////////
// Stmt
//////////////////////////////////////////////////////////////////////////
override Void enterStmt(Stmt stmt) { stmtStack.push(stmt) }
override Void visitStmt(Stmt stmt)
{
stmtStack.pop
switch (stmt.id)
{
case StmtId.expr: resolveExprStmt((ExprStmt)stmt)
case StmtId.forStmt: resolveFor((ForStmt)stmt)
case StmtId.breakStmt: resolveBreak((BreakStmt)stmt)
case StmtId.continueStmt: resolveContinue((ContinueStmt)stmt)
case StmtId.localDef: resolveLocalVarDef((LocalDefStmt)stmt)
}
}
private Void resolveExprStmt(ExprStmt stmt)
{
// stand alone expr statements, shouldn't be left on the stack
stmt.expr = stmt.expr.noLeave
}
private Void resolveLocalVarDef(LocalDefStmt def)
{
// check for type inference
if (def.ctype == null)
def.ctype = def.init.ctype.inferredAs
// bind to scope as a method variable
bindToMethodVar(def)
// if init is null, then we default the variable to null (Fan
// doesn't do true definite assignment checking since most local
// variables use type inference anyhow)
if (def.init == null && !def.isCatchVar)
def.init = LiteralExpr.makeNullLiteral(def.location, ns)
// turn init into full assignment
if (def.init != null)
def.init = BinaryExpr.makeAssign(LocalVarExpr.make(def.location, def.var), def.init)
}
private Void resolveFor(ForStmt stmt)
{
// don't leave update expression on the stack
if (stmt.update != null) stmt.update = stmt.update.noLeave
}
private Void resolveBreak(BreakStmt stmt)
{
// find which loop we're inside of (checked in CheckErrors)
stmt.loop = findLoop
}
private Void resolveContinue(ContinueStmt stmt)
{
// find which loop we're inside of (checked in CheckErrors)
stmt.loop = findLoop
}
//////////////////////////////////////////////////////////////////////////
// Expr
//////////////////////////////////////////////////////////////////////////
override Expr visitExpr(Expr expr)
{
// resolve the expression
expr = resolveExpr(expr)
// expr type must be resolved at this point
if ((Obj?)expr.ctype == null)
throw err("Expr type not resolved: ${expr.id}: ${expr}", expr.location)
// if we resolved to a generic parameter like V or K,
// then use its real underlying type
if (expr.ctype.isGenericParameter)
expr.ctype = expr.ctype.raw
return expr
}
private Expr resolveExpr(Expr expr)
{
switch (expr.id)
{
case ExprId.slotLiteral: return resolveSlotLiteral((SlotLiteralExpr)expr)
case ExprId.listLiteral: return resolveList((ListLiteralExpr)expr)
case ExprId.mapLiteral: return resolveMap((MapLiteralExpr)expr)
case ExprId.boolNot:
case ExprId.cmpNull:
case ExprId.cmpNotNull: expr.ctype = ns.boolType
case ExprId.assign: return resolveAssign((BinaryExpr)expr)
case ExprId.elvis: resolveElvis((BinaryExpr)expr)
case ExprId.same:
case ExprId.notSame:
case ExprId.boolOr:
case ExprId.boolAnd:
case ExprId.isExpr: expr.ctype = ns.boolType
case ExprId.isnotExpr: expr.ctype = ns.boolType
case ExprId.asExpr: expr.ctype = ((TypeCheckExpr)expr).check.toNullable
case ExprId.call: return resolveCall((CallExpr)expr)
case ExprId.construction: return resolveConstruction((CallExpr)expr)
case ExprId.shortcut: return resolveShortcut((ShortcutExpr)expr)
case ExprId.thisExpr: return resolveThis((ThisExpr)expr)
case ExprId.superExpr: return resolveSuper((SuperExpr)expr)
case ExprId.unknownVar: return resolveVar((UnknownVarExpr)expr)
case ExprId.storage: return resolveStorage((UnknownVarExpr)expr)
case ExprId.coerce: expr.ctype = ((TypeCheckExpr)expr).check
case ExprId.ternary: resolveTernary((TernaryExpr)expr)
case ExprId.withSub: resolveWithSub((WithSubExpr)expr)
case ExprId.curry: return resolveCurry((CurryExpr)expr)
case ExprId.closure: resolveClosure((ClosureExpr)expr)
}
return expr
}
**
** Resolve slot literal
**
private Expr resolveSlotLiteral(SlotLiteralExpr expr)
{
slot := expr.parent.slot(expr.name)
if (slot == null)
{
err("Unknown slot literal '${expr.parent.signature}.${expr.name}'", expr.location)
expr.ctype = ns.error
return expr
}
expr.ctype = slot is CField ? ns.fieldType : ns.methodType
expr.slot = slot
return expr
}
**
** Resolve list literal
**
private Expr resolveList(ListLiteralExpr expr)
{
if (expr.explicitType != null)
{
expr.ctype = expr.explicitType
}
else
{
// infer from list item expressions
v := Expr.commonType(ns, expr.vals)
expr.ctype = v.toListOf
}
return expr
}
**
** Resolve map literal
**
private Expr resolveMap(MapLiteralExpr expr)
{
if (expr.explicitType != null)
{
expr.ctype = expr.explicitType
}
else
{
// infer from key/val expressions
k := Expr.commonType(ns, expr.keys).toNonNullable
v := Expr.commonType(ns, expr.vals)
expr.ctype = MapType.make(k, v)
}
return expr
}
**
** Resolve this keyword expression
**
private Expr resolveThis(ThisExpr expr)
{
if (inClosure)
{
loc := expr.location
closure := curType.closure
// if the closure is in a static slot, report an error
if (closure.enclosingSlot.isStatic)
{
expr.ctype = ns.error
err("Cannot access 'this' within closure of static context", loc)
return expr
}
// otherwise replace this with $this field access
return FieldExpr.make(loc, ThisExpr.make(loc), closure.outerThisField)
}
expr.ctype = curType
return expr
}
**
** Resolve super keyword expression
**
private Expr resolveSuper(SuperExpr expr)
{
if (inClosure)
{
// it would be nice to support super from within a closure,
// but the Java VM has the stupid restriction that invokespecial
// cannot be used outside of the class - we could potentially
// work around this using a wrapper method - but for now we will
// just disallow it
err("Invalid use of 'super' within closure", expr.location)
expr.ctype = ns.error
return expr
}
if (expr.explicitType != null)
expr.ctype = expr.explicitType
else
expr.ctype = curType.base
return expr
}
**
** Resolve an assignment operation
**
private Expr resolveAssign(BinaryExpr expr)
{
// if lhs has synthetic coercion we need to remove it;
// this can occur when resolving a FFI field
if (expr.lhs.id == ExprId.coerce && expr.lhs.synthetic)
expr.lhs = ((TypeCheckExpr)expr.lhs).target
// check for left hand side the [] shortcut, because []= is set
shortcut := expr.lhs as ShortcutExpr
if (shortcut != null && shortcut.op == ShortcutOp.get)
{
shortcut.op = ShortcutOp.set
shortcut.name = "set"
shortcut.args.add(expr.rhs)
shortcut.method = null
return resolveCall(shortcut)
}
// check for left hand side the -> shortcut, because a->x=b is trap.a("x", [b])
call := expr.lhs as CallExpr
if (call != null && call.isDynamic)
{
call.args.add(expr.rhs)
return resolveCall(call)
}
// assignment is typed by lhs
expr.ctype = expr.lhs.ctype
return expr
}
**
** Resolve an UnknownVar to its replacement node.
**
private Expr resolveVar(UnknownVarExpr var)
{
// if there is no target, attempt to bind to local variable
if (var.target == null)
{
// attempt to a name in the current scope
binding := resolveLocal(var.name)
if (binding != null)
return LocalVarExpr.make(var.location, binding)
}
// at this point it can't be a local variable, so it must be
// a slot on either myself or the variable's target
return CallResolver.make(compiler, curType, curMethod, var).resolve
}
**
** Resolve storage operator
**
private Expr resolveStorage(UnknownVarExpr var)
{
// resolve as normal unknown variable
resolved := resolveVar(var)
// handle case where we have a local variable hiding a
// field since the @x is assumed to be @this.x
if (resolved.id === ExprId.localVar)
{
field := curType.field(var.name)
if (field != null)
resolved = FieldExpr(var.location, ThisExpr(var.location), field)
}
// is we can't resolve as field, then this is an error
if (resolved.id !== ExprId.field)
{
if (resolved.ctype !== ns.error)
err("Invalid use of field storage operator '@'", var.location)
return resolved
}
f := resolved as FieldExpr
f.useAccessor = false
if (f.field is FieldDef) ((FieldDef)f.field).flags |= FConst.Storage
return f
}
**
** Resolve "x ?: y" expression
**
private Expr resolveElvis(BinaryExpr expr)
{
expr.ctype = CType.common(ns, [expr.lhs.ctype, expr.rhs.ctype]).toNullable
return expr
}
**
** Resolve "x ? y : z" ternary expression
**
private Expr resolveTernary(TernaryExpr expr)
{
if (expr.trueExpr.id === ExprId.nullLiteral)
expr.ctype = expr.falseExpr.ctype.toNullable
else if (expr.falseExpr.id === ExprId.nullLiteral)
expr.ctype = expr.trueExpr.ctype.toNullable
else
expr.ctype = CType.common(ns, [expr.trueExpr.ctype, expr.falseExpr.ctype])
return expr
}
**
** Resolve with-sub expression
**
private Expr resolveWithSub(WithSubExpr sub)
{
// update if sub-expr was resolved as 'with.add(expr)'
if (sub.add != null)
sub.expr = CallExpr.makeWithMethod(sub.location, WithBaseExpr.make(sub.withBlock), sub.add, [sub.expr])
// we never leave sub-expr on the stack
sub.expr = sub.expr.noLeave
sub.ctype = sub.expr.ctype
return sub
}
**
** Resolve a call to it's Method and return type.
**
private Expr resolveCall(CallExpr call)
{
// dynamic calls are just syntactic sugar for Obj.trap
if (call.isDynamic)
{
call.method = ns.objTrap
call.ctype = ns.objType.toNullable
return call
}
// if this is a constructor chained call to a FFI
// super-class then route to the FFI bridge to let it handle
if (call.isCtorChain && curType.base.isForeign)
return curType.base.bridge.resolveConstructorChain(call)
// if there is no target, attempt to bind to local variable
if (call.target == null)
{
// attempt to a name in the current scope
binding := resolveLocal(call.name)
if (binding != null)
return resolveCallOnLocalVar(call, LocalVarExpr.make(call.location, binding))
}
return CallResolver.make(compiler, curType, curMethod, call).resolve
}
**
** Resolve the () operator on a local variable - if the local
** is a Method, then () is syntactic sugar for Method.callx()
**
private Expr resolveCallOnLocalVar(CallExpr call, LocalVarExpr binding)
{
// if binding isn't a sys::Func then no can do
if (!binding.ctype.fits(ns.funcType))
{
if (binding.ctype != ns.error)
err("Cannot call local variable '$call.name' like a function", call.location)
call.ctype = ns.error
return call
}
// can only handle zero to eight arguments; I could wrap up the
// arguments into a List and use call(List) - but methods with
// that many arguments are just inane so tough luck
if (call.args.size > 8)
{
err("Tough luck - cannot use () operator with more than 8 arguments, use call(List)", call.location)
call.ctype = ns.error
return call
}
// invoking the () operator on a sys::Func is syntactic
// sugar for invoking one of the Func.callX methods
callx := binding.ctype.method("call${call.args.size}")
return CallExpr.makeWithMethod(call.location, binding, callx, call.args)
}
**
** Resolve a construction call Type(args)
**
private Expr resolveConstruction(CallExpr call)
{
base := call.target.ctype
// route FFI constructors to bridge
if (base.isForeign) return base.bridge.resolveConstruction(call)
// construction always resolves to base type (we
// double check this in CheckErrors)
call.ctype = base
// check for fromStr
if (call.args.size == 1 && call.args.first.ctype.isStr)
{
fromStr := base.method("fromStr")
if (fromStr != null && fromStr.parent == base)
{
call.method = fromStr
return call
}
}
// resolve make
call.method = base.method("make")
if (call.method == null)
err("Unknown construction method '${base.qname}.make'", call.location)
return call
}
**
** Resolve ShortcutExpr.
**
private Expr resolveShortcut(ShortcutExpr expr)
{
// if this is an indexed assigment such as x[y] += z
if (expr.isAssign && expr.target.id === ExprId.shortcut)
return resolveIndexedAssign(expr)
// if a binary operation
if (expr.args.size == 1)
{
// extract lhs and rhs
lhs := expr.target
rhs := expr.args.first
// if arg is Range, then get() is really slice()
if (expr.op === ShortcutOp.get && rhs.ctype.isRange)
{
expr.op = ShortcutOp.slice
expr.name = "slice"
}
// string concat is always optimized, and performs a bit
// different since a non-string can be used as the lhs
if (expr.isStrConcat)
{
expr.ctype = ns.strType
expr.method = ns.strPlus
return ConstantFolder.make(compiler).fold(expr)
}
}
// resolve the call, if optimized, then return it immediately
result := resolveCall(expr)
if (result !== expr) return result
// the comparision operations are special case that call a method
// that return an Int, but leave a Bool on the stack (we also handle
// specially in assembler)
switch (expr.opToken)
{
case Token.lt:
case Token.ltEq:
case Token.gt:
case Token.gtEq:
expr.ctype = ns.boolType
}
return expr
}
**
** If we have an assignment against an indexed shortcut
** such as x[y] += z, then process specially to return
** a IndexedAssignExpr subclass of ShortcutExpr.
**
private Expr resolveIndexedAssign(ShortcutExpr orig)
{
// if target is in error, don't bother
if (orig.target.ctype === ns.error)
{
orig.ctype = ns.error
return orig
}
// we better have a x[y] indexed get expression
if (orig.target.id != ExprId.shortcut && orig.target->op === ShortcutOp.get)
{
err("Expected indexed expression", orig.location)
return orig
}
// wrap the shorcut as an IndexedAssignExpr
expr := IndexedAssignExpr.makeFrom(orig)
// resolve it normally - if the orig is "x[y] += z" then we
// are resolving Int.plus here - the target is "x[y]" and should
// already be resolved
resolveCall(expr)
// we need two scratch variables to manipulate the stack cause
// .NET is lame when it comes to doing anything with the stack
// - scratchA: target collection
// - scratchB: index
target := (ShortcutExpr)expr.target
expr.scratchA = curMethod.addLocalVar(target.target.ctype, null, null)
expr.scratchB = curMethod.addLocalVar(target.args[0].ctype, null, null)
// resolve the set method which matches
// the get method on the target
get := ((ShortcutExpr)expr.target).method
set := get.parent.method("set")
if (set == null || set.params.size != 2 || set.isStatic ||
set.params[0].paramType.toNonNullable != get.params[0].paramType.toNonNullable ||
set.params[1].paramType.toNonNullable != get.returnType.toNonNullable)
err("No matching 'set' method for '$get.qname'", orig.location)
else
expr.setMethod = set
// return the new IndexedAssignExpr
return expr
}
**
** CurryExpr
**
private Expr resolveCurry(CurryExpr expr)
{
// short circuit if operand is in error
if (expr.operand.ctype == ns.error)
{
expr.ctype = ns.error
return expr
}
// if the operand isn't a CallExpr, then we have a problem
if (expr.operand.id !== ExprId.call)
{
err("Invalid operand '$expr.operand.id' for curry operator", expr.location)
expr.ctype = ns.error
return expr
}
// use CurryResolver for all the heavy lifting
return CurryResolver.make(compiler, curType, curryCount++, expr).resolve
}
**
** ClosureExpr will just output its substitute expression. But we take
** this opportunity to capture the local variables in the closure's scope
** and cache them on the ClosureExpr. We also do variable name checking.
**
private Void resolveClosure(ClosureExpr expr)
{
// save away current locals in scope
expr.enclosingLocals = localsInScope
// make sure none of the closure's parameters
// conflict with the locals in scope
expr.doCall.paramDefs.each |ParamDef p|
{
if (expr.enclosingLocals.containsKey(p.name))
err("Closure parameter '$p.name' is already defined in current block", p.location)
}
}
//////////////////////////////////////////////////////////////////////////
// Scope
//////////////////////////////////////////////////////////////////////////
**
** Setup the MethodVars for the parameters.
**
private Void initMethodVars()
{
m := curMethod
reg := m.isStatic ? 0 : 1
m.paramDefs.each |ParamDef p|
{
var := MethodVar.makeForParam(reg++, p)
m.vars.add(var)
}
}
**
** Bind the specified local variable definition to a
** MethodVar (and register number).
**
private Void bindToMethodVar(LocalDefStmt def)
{
// make sure it doesn't exist in the current scope
if (resolveLocal(def.name) != null)
err("Variable '$def.name' is already defined in current block", def.location)
// create and add it
def.var = curMethod.addLocalVar(def.ctype, def.name, currentBlock)
}
**
** Resolve a local variable using current scope based on
** the block stack and possibly the scope of a closure.
**
private MethodVar? resolveLocal(Str name)
{
// if not in method, then we can't have a local
if (curMethod == null) return null
// attempt to a name in the current scope
binding := curMethod.vars.find |MethodVar var->Bool|
{
return var.name == name && isBlockInScope(var.scope)
}
if (binding != null) return binding
// if a closure, check parent scope
if (inClosure)
{
closure := curType.closure
binding = closure.enclosingLocals[name]
if (binding != null)
{
// mark the local var as being used in a closure so that
// we know to generate a cvar field for it in ClosureVars
binding.usedInClosure = true
// mark this closure as using cvars
closure.usesCvars = true
// mark the enclosing method and recursively
// any outer closures as needing cvars
((MethodDef)closure.enclosingSlot).needsCvars = true
for (p := closure.enclosingClosure; p != null; p = p.enclosingClosure)
p.doCall.needsCvars = true
return binding
}
}
// not found
return null
}
**
** Get a list of all the local method variables that
** are currently in scope.
**
private Str:MethodVar localsInScope()
{
Str:MethodVar acc := inClosure ?
curType.closure.enclosingLocals.dup :
Str:MethodVar[:]
curMethod.vars.each |MethodVar var|
{
if (isBlockInScope(var.scope))
acc[var.name] = var
}
return acc
}
**
** Get the current block which defines our scope. We make
** a special case for "for" loops which can declare variables.
**
private Block currentBlock()
{
if (stmtStack.peek is ForStmt)
return ((ForStmt)stmtStack.peek).block
else
return blockStack.peek
}
**
** Check if the specified block is currently in scope. We make
** a specialcase for "for" loops which can declare variables.
**
private Bool isBlockInScope(Block? block)
{
// the null block within the whole method (ctorChains or defaultParams)
if (block == null) return true
// special case for "for" loops
if (stmtStack.peek is ForStmt)
{
if (((ForStmt)stmtStack.peek).block === block)
return true
}
// look in block stack which models scope chain
return blockStack.any |Block b->Bool| { return b === block }
}
//////////////////////////////////////////////////////////////////////////
// StmtStack
//////////////////////////////////////////////////////////////////////////
private Stmt? findLoop()
{
for (i:=stmtStack.size-1; i>=0; --i)
{
stmt := stmtStack[i]
if (stmt.id === StmtId.whileStmt) return stmt
if (stmt.id === StmtId.forStmt) return stmt
}
return null
}
//////////////////////////////////////////////////////////////////////////
// BlockStack
//////////////////////////////////////////////////////////////////////////
override Void enterBlock(Block block) { blockStack.push(block) }
override Void exitBlock(Block block) { blockStack.pop }
//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////
Stmt[] stmtStack := Stmt[,] // statement stack
Block[] blockStack := Block[,] // block stack used for scoping
Bool inClosure := false // are we inside a closure's block
Int curryCount := 0 // total number of curry exprs processed
}