Move to using DB-levels (not DB-indexes) in SExpr0. This change includes both index and level, and performs a runtime check. (#11987)

changelog_begin
changelog_end

add runtime check in freeVars: determination that a variable is-free using levels instead of indexes

remove DB-indexes and runtime check; simplify freeVars computation in closure-conversion
This commit is contained in:
nickchapman-da 2021-12-09 14:17:15 +00:00 committed by GitHub
parent 0a17087b99
commit fdf7431ad1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 81 additions and 97 deletions

View File

@ -90,20 +90,21 @@ object PlaySpeedy {
def examples: Map[String, (Int, SExpr)] = {
def num(n: Long): SExpr = SEValue(SInt64(n))
def mkVar(level: Int) = SEVarLevel(level)
// The trailing numeral is the number of args at the scala level
// The trailing numeral is the number of args at the scala mkVar
def decrement1(x: SExpr): SExpr = SEApp(SEBuiltin(SBSubInt64), List(x, SEValue(SInt64(1))))
val decrement = SEAbs(1, decrement1(SEVar(1)))
val decrement = SEAbs(1, decrement1(mkVar(0)))
def subtract2(x: SExpr, y: SExpr): SExpr = SEApp(SEBuiltin(SBSubInt64), List(x, y))
val subtract = SEAbs(2, subtract2(SEVar(2), SEVar(1)))
val subtract = SEAbs(2, subtract2(mkVar(0), mkVar(1)))
def twice2(f: SExpr, x: SExpr): SExpr = SEApp(f, List(SEApp(f, List(x))))
val twice = SEAbs(2, twice2(SEVar(2), SEVar(1)))
val twice = SEAbs(2, twice2(mkVar(3), mkVar(4)))
def thrice2(f: SExpr, x: SExpr): SExpr = SEApp(f, List(SEApp(f, List(SEApp(f, List(x))))))
val thrice = SEAbs(2, thrice2(SEVar(2), SEVar(1)))
val thrice = SEAbs(2, thrice2(mkVar(0), mkVar(1)))
val examples = List(
(
@ -142,7 +143,10 @@ object PlaySpeedy {
List(num(21)),
SEApp(
twice,
List(SEAbs(1, subtract2(SEVar(1), subtract2(SEVar(4), SEVar(2)))), SEVar(2)),
List(
SEAbs(1, subtract2(mkVar(3), subtract2(mkVar(0), mkVar(2)))),
mkVar(1),
),
),
), //100
),

View File

@ -18,36 +18,36 @@ import scala.annotation.tailrec
private[speedy] object ClosureConversion {
import source.SEVarLevel
private[speedy] def closureConvert(source0: source.SExpr): target.SExpr = {
case class Abs(a: Int) // absolute variable index, determined by tracking sourceDepth
case class Env(sourceDepth: Int, mapping: Map[SEVarLevel, target.SELoc], targetDepth: Int) {
case class Env(sourceDepth: Int, mapping: Map[Abs, target.SELoc], targetDepth: Int) {
def lookup(abs: Abs): target.SELoc = {
mapping.get(abs) match {
def lookup(v: SEVarLevel): target.SELoc = {
mapping.get(v) match {
case Some(loc) => loc
case None =>
throw sys.error(s"lookup($abs),in:$mapping")
case None => sys.error(s"lookup($v),in:$mapping")
}
}
def extend(n: Int): Env = {
// Create mappings for `n` new stack items, and combine with the (unshifted!) existing mapping.
val m2 = (0 until n).view.map { i =>
val abs = Abs(sourceDepth + i)
(abs, target.SELocAbsoluteS(targetDepth + i))
val v = SEVarLevel(sourceDepth + i)
(v, target.SELocAbsoluteS(targetDepth + i))
}
Env(sourceDepth + n, mapping ++ m2, targetDepth + n)
}
def absBody(arity: Int, fvs: List[Abs]): Env = {
val newRemapsF: Map[Abs, target.SELoc] = fvs.view.zipWithIndex.map { case (abs, i) =>
abs -> target.SELocF(i)
}.toMap
def absBody(arity: Int, freeVars: List[SEVarLevel]): Env = {
val newRemapsF =
freeVars.view.zipWithIndex.map { case (v, i) =>
v -> target.SELocF(i)
}.toMap
val newRemapsA = (0 until arity).view.map { case i =>
val abs = Abs(sourceDepth + i)
abs -> target.SELocA(i)
val v = SEVarLevel(sourceDepth + i)
v -> target.SELocA(i)
}
// The keys in newRemapsF and newRemapsA are disjoint
val m1 = newRemapsF ++ newRemapsA
@ -85,7 +85,7 @@ private[speedy] object ClosureConversion {
* forms correspond to the source expression forms: specifically, the location of
* recursive expression instances (values of type SExpr).
*
* For expression forms with no recursive instance (i.e. SEVar, SEVal), there are
* For expression forms with no recursive instance (i.e. SEVarLevel, SEVal), there are
* no corresponding continuation forms.
*
* For expression forms with a single recursive instance (i.e. SELocation), there
@ -112,7 +112,7 @@ private[speedy] object ClosureConversion {
final case class Location(loc: Ref.Location) extends Cont
final case class Abs(arity: Int, fvs: List[target.SELoc]) extends Cont
final case class Abs(arity: Int, freeLocs: List[target.SELoc]) extends Cont
final case class App1(env: Env, args: List[source.SExpr]) extends Cont
@ -176,9 +176,8 @@ private[speedy] object ClosureConversion {
// Going Down: match on expression form...
case Down(exp, env) =>
exp match {
case source.SEVar(r) =>
val abs = Abs(env.sourceDepth - r)
loop(Up(env.lookup(abs)), conts)
case v: SEVarLevel =>
loop(Up(env.lookup(v)), conts)
case source.SEVal(x) => loop(Up(target.SEVal(x)), conts)
case source.SEBuiltin(x) => loop(Up(target.SEBuiltin(x)), conts)
@ -188,11 +187,10 @@ private[speedy] object ClosureConversion {
loop(Down(body, env), Cont.Location(loc) :: conts)
case source.SEAbs(arity, body) =>
val fvsAsListAbs = freeVars(body, arity).toList.sorted.map { r =>
Abs(env.sourceDepth - r)
}
val fvs = fvsAsListAbs.map { abs => env.lookup(abs) }
loop(Down(body, env.absBody(arity, fvsAsListAbs)), Cont.Abs(arity, fvs) :: conts)
val freeVars =
computeFreeVars(body, env.sourceDepth).toList.sortBy(_.level)
val freeLocs = freeVars.map { v => env.lookup(v) }
loop(Down(body, env.absBody(arity, freeVars)), Cont.Abs(arity, freeLocs) :: conts)
case source.SEApp(fun, args) =>
loop(Down(fun, env), Cont.App1(env, args) :: conts)
@ -234,9 +232,9 @@ private[speedy] object ClosureConversion {
val body = result
loop(Up(target.SELocation(loc, body)), conts)
case Cont.Abs(arity, fvs) =>
case Cont.Abs(arity, freeLocs) =>
val body = result
loop(Up(target.SEMakeClo(fvs, arity, body)), conts)
loop(Up(target.SEMakeClo(freeLocs, arity, body)), conts)
case Cont.App1(env, args) =>
val fun = result
@ -319,56 +317,40 @@ private[speedy] object ClosureConversion {
loop(Down(source0, Env()), Nil)
}
/** Compute the free variables in a speedy expression.
* The returned free variables are de bruijn indices adjusted to the stack of the caller.
*/
private[this] def freeVars(expr0: source.SExpr, depth0: Int): Set[Int] = {
/** Compute the free variables of a speedy expression */
private[this] def computeFreeVars(expr0: source.SExpr, sourceDepth: Int): Set[SEVarLevel] = {
@tailrec // woo hoo, stack safe!
def go(acc: Set[Int], work: List[(source.SExpr, Int)]): Set[Int] = {
def go(acc: Set[SEVarLevel], work: List[source.SExpr]): Set[SEVarLevel] = {
// 'acc' is the (accumulated) set of free variables we have found so far.
// 'work' is a list of source expressions (paired with their depth) which we still have to process.
// 'work' is a list of source expressions which we still have to process.
work match {
case Nil => acc // final result
case (expr, depth) :: work => {
case expr :: work => {
expr match {
case source.SEVar(rel) =>
if (rel > depth) {
val callerRel = rel - depth // adjust to caller's environment
go(acc + callerRel, work)
case v @ SEVarLevel(level) =>
if (level < sourceDepth) {
go(acc + v, work)
} else {
go(acc, work)
}
case _: source.SEVal => go(acc, work)
case _: source.SEBuiltin => go(acc, work)
case _: source.SEValue => go(acc, work)
case source.SELocation(_, body) =>
go(acc, (body, depth) :: work)
case source.SEApp(fun, args) =>
go(acc, (fun :: args).map(e => (e, depth)) ++ work)
case source.SEAbs(n, body) =>
go(acc, (body, depth + n) :: work)
case source.SELocation(_, body) => go(acc, body :: work)
case source.SEApp(fun, args) => go(acc, fun :: args ++ work)
case source.SEAbs(_, body) => go(acc, body :: work)
case source.SECase(scrut, alts) =>
val moreWork = alts.map { case source.SCaseAlt(pat, body) =>
val n = pat.numArgs
(body, depth + n)
}
go(acc, (scrut, depth) :: moreWork ++ work)
case source.SELet(bounds, body) =>
val moreWork = bounds.zipWithIndex.map { case (bound, n) =>
(bound, depth + n)
}
go(acc, (body, depth + bounds.length) :: moreWork ++ work)
case source.SELabelClosure(_, expr) =>
go(acc, (expr, depth) :: work)
case source.SETryCatch(body, handler) =>
go(acc, (handler, 1 + depth) :: (body, depth) :: work)
case source.SEScopeExercise(body) =>
go(acc, (body, depth) :: work)
val bodies = alts.map { case source.SCaseAlt(_, body) => body }
go(acc, scrut :: bodies ++ work)
case source.SELet(bounds, body) => go(acc, body :: bounds ++ work)
case source.SELabelClosure(_, expr) => go(acc, expr :: work)
case source.SETryCatch(body, handler) => go(acc, handler :: body :: work)
case source.SEScopeExercise(body) => go(acc, body :: work)
}
}
}
}
go(Set.empty, List((expr0, depth0)))
go(Set.empty, List(expr0))
}
}

View File

@ -27,7 +27,7 @@ import scala.reflect.ClassTag
/** Compiles LF expressions into Speedy expressions.
* This includes:
* - Writing variable references into de Bruijn indices.
* - Translating variable references into de Bruijn levels.
* - Closure conversion: EAbs turns into SEMakeClo, which creates a closure by copying free variables into a closure object.
* - Rewriting of update and scenario actions into applications of builtin functions that take an "effect" token.
*
@ -90,14 +90,6 @@ private[lf] object Compiler {
private val SEGetTime = s.SEBuiltin(SBGetTime)
private def SBCompareNumeric(b: SBuiltinPure) =
s.SEAbs(3, s.SEApp(s.SEBuiltin(b), List(s.SEVar(2), s.SEVar(1))))
private val SBLessNumeric = SBCompareNumeric(SBLess)
private val SBLessEqNumeric = SBCompareNumeric(SBLessEq)
private val SBGreaterNumeric = SBCompareNumeric(SBGreater)
private val SBGreaterEqNumeric = SBCompareNumeric(SBGreaterEq)
private val SBEqualNumeric = SBCompareNumeric(SBEqual)
private val SBEToTextNumeric = s.SEAbs(1, s.SEBuiltin(SBToText))
private val SENat: Numeric.Scale => Some[s.SEValue] =
@ -182,7 +174,7 @@ private[lf] final class Compiler(
varIndices: Map[VarRef, Position],
) {
def toSEVar(p: Position): s.SEVar = s.SEVar(position - p.idx)
def toSEVar(p: Position) = s.SEVarLevel(p.idx)
def nextPosition = Position(position)
@ -214,14 +206,14 @@ private[lf] final class Compiler(
private[this] def vars: List[VarRef] = varIndices.keys.toList
private[this] def lookupVar(varRef: VarRef): Option[s.SEVar] =
private[this] def lookupVar(varRef: VarRef): Option[s.SExpr] =
varIndices.get(varRef).map(toSEVar)
def lookupExprVar(name: ExprVarName): s.SEVar =
def lookupExprVar(name: ExprVarName): s.SExpr =
lookupVar(EVarRef(name))
.getOrElse(throw CompilationError(s"Unknown variable: $name. Known: ${vars.mkString(",")}"))
def lookupTypeVar(name: TypeVarName): Option[s.SEVar] =
def lookupTypeVar(name: TypeVarName): Option[s.SExpr] =
lookupVar(TVarRef(name))
}
@ -477,7 +469,7 @@ private[lf] final class Compiler(
case EVal(ref) =>
s.SEVal(t.LfDefRef(ref))
case EBuiltin(bf) =>
compileBuiltin(bf)
compileBuiltin(env, bf)
case EPrimCon(con) =>
compilePrimCon(con)
case EPrimLit(lit) =>
@ -587,9 +579,24 @@ private[lf] final class Compiler(
}
@inline
private[this] def compileBuiltin(bf: BuiltinFunction): s.SExpr =
private[this] def compileIdentity(env: Env) = s.SEAbs(1, s.SEVarLevel(env.position))
@inline
private[this] def compileBuiltin(env: Env, bf: BuiltinFunction): s.SExpr = {
def SBCompareNumeric(b: SBuiltinPure) = {
val d = env.position
s.SEAbs(3, s.SEApp(s.SEBuiltin(b), List(s.SEVarLevel(d + 1), s.SEVarLevel(d + 2))))
}
val SBLessNumeric = SBCompareNumeric(SBLess)
val SBLessEqNumeric = SBCompareNumeric(SBLessEq)
val SBGreaterNumeric = SBCompareNumeric(SBGreater)
val SBGreaterEqNumeric = SBCompareNumeric(SBGreaterEq)
val SBEqualNumeric = SBCompareNumeric(SBEqual)
bf match {
case BCoerceContractId => s.SEAbs.identity
case BCoerceContractId => compileIdentity(env)
// Numeric Comparisons
case BLessNumeric => SBLessNumeric
case BLessEqNumeric => SBLessEqNumeric
@ -712,6 +719,7 @@ private[lf] final class Compiler(
case BAnyExceptionMessage => SBAnyExceptionMessage
})
}
}
@inline
private[this] def compilePrimCon(con: PrimCon): s.SExpr =
@ -1345,7 +1353,7 @@ private[lf] final class Compiler(
ifaceId: Identifier,
): (t.SDefinitionRef, SDefinition) =
t.ImplementsDefRef(tmplId, ifaceId) ->
SDefinition(unsafeClosureConvert(s.SEAbs.identity))
SDefinition(unsafeClosureConvert(compileIdentity(Env.Empty)))
// Compile the implementation of an interface method.
private[this] def compileImplementsMethod(

View File

@ -13,7 +13,7 @@ package speedy
*
* 1: convert from LF
* - reducing binding forms (update & scenario becoming builtins)
* - moving to de Bruijn indexing for variable
* - convert named variables to de Bruijn levels
* - moving to multi-argument applications and abstractions.
*
* 2: closure conversion
@ -54,13 +54,11 @@ private[speedy] object SExpr0 {
sealed abstract class SExpr extends Product with Serializable
/** Reference to a variable. 'index' is the 1-based de Bruijn index,
* that is, SEVar(1) points to the nearest enclosing variable binder.
* which could be an SELam, SELet, or a binding variant of SECasePat.
/** Reference to a variable. 'level' is the 0-based de Bruijn LEVEL (not INDEX)
* https://en.wikipedia.org/wiki/De_Bruijn_index
* This expression form is only allowed prior to closure conversion
*/
final case class SEVar(index: Int) extends SExpr
final case class SEVarLevel(level: Int) extends SExpr
/** Reference to a value. On first lookup the evaluated expression is
* stored in 'cached'.
@ -81,14 +79,6 @@ private[speedy] object SExpr0 {
/** Lambda abstraction. Transformed to SEMakeClo during closure conversion */
final case class SEAbs(arity: Int, body: SExpr) extends SExpr
object SEAbs {
// Helper for constructing abstraction expressions:
// SEAbs(1) { ... }
def apply(arity: Int)(body: SExpr): SExpr = SEAbs(arity, body)
val identity: SEAbs = SEAbs(1, SEVar(1))
}
/** Pattern match. */
final case class SECase(scrut: SExpr, alts: List[SCaseAlt]) extends SExpr