Skip to content

Commit 63435f9

Browse files
authored
Refactor/replace environments by substitution (#787)
1 parent 864bd99 commit 63435f9

File tree

14 files changed

+386
-138
lines changed

14 files changed

+386
-138
lines changed

effekt/js/src/main/scala/effekt/Backend.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ package effekt
33
class Backend {
44
val compiler = generator.js.JavaScriptWeb()
55
val runner = ()
6+
val name = "js-web"
67
}

effekt/jvm/src/test/scala/effekt/core/OptimizerTests.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class OptimizerTests extends CoreTests {
3636
def normalize(input: String, expected: String)(using munit.Location) =
3737
assertTransformsTo(input, expected) { tree =>
3838
val anfed = BindSubexpressions.transform(tree)
39-
val normalized = Normalizer.normalize(Set(mainSymbol), anfed, 50)
39+
val normalized = Normalizer.normalize(Set(mainSymbol), anfed, 50, false)
4040
Deadcode.remove(mainSymbol, normalized)
4141
}
4242

effekt/jvm/src/test/scala/effekt/core/VMTests.scala

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ class VMTests extends munit.FunSuite {
533533
dynamicDispatches = 0,
534534
patternMatches = 400,
535535
branches = 1487,
536-
pushedFrames = 1352,
536+
pushedFrames = 1185,
537537
poppedFrames = 1409,
538538
allocations = 54,
539539
closures = 0,
@@ -549,7 +549,7 @@ class VMTests extends munit.FunSuite {
549549
dynamicDispatches = 0,
550550
patternMatches = 0,
551551
branches = 210,
552-
pushedFrames = 379,
552+
pushedFrames = 378,
553553
poppedFrames = 377,
554554
allocations = 0,
555555
closures = 0,
@@ -613,7 +613,7 @@ class VMTests extends munit.FunSuite {
613613
dynamicDispatches = 0,
614614
patternMatches = 4,
615615
branches = 701,
616-
pushedFrames = 874,
616+
pushedFrames = 702,
617617
poppedFrames = 880,
618618
allocations = 4,
619619
closures = 0,
@@ -660,13 +660,13 @@ class VMTests extends munit.FunSuite {
660660

661661
examplesDir / "casestudies" / "scheduler.effekt.md" -> Some(Summary(
662662
staticDispatches = 60,
663-
dynamicDispatches = 8,
663+
dynamicDispatches = 7,
664664
patternMatches = 95,
665665
branches = 41,
666-
pushedFrames = 106,
666+
pushedFrames = 105,
667667
poppedFrames = 106,
668668
allocations = 73,
669-
closures = 8,
669+
closures = 7,
670670
variableReads = 29,
671671
variableWrites = 18,
672672
resets = 1,
@@ -695,40 +695,40 @@ class VMTests extends munit.FunSuite {
695695
dynamicDispatches = 783,
696696
patternMatches = 13502,
697697
branches = 14892,
698-
pushedFrames = 28523,
699-
poppedFrames = 28499,
698+
pushedFrames = 28210,
699+
poppedFrames = 28186,
700700
allocations = 7923,
701701
closures = 521,
702702
variableReads = 6742,
703703
variableWrites = 1901,
704-
resets = 806,
705-
shifts = 855,
706-
resumes = 839
704+
resets = 778,
705+
shifts = 229,
706+
resumes = 213
707707
)),
708708

709709
examplesDir / "casestudies" / "anf.effekt.md" -> Some(Summary(
710710
staticDispatches = 4775,
711711
dynamicDispatches = 443,
712712
patternMatches = 7272,
713713
branches = 8110,
714-
pushedFrames = 16275,
715-
poppedFrames = 16260,
714+
pushedFrames = 16101,
715+
poppedFrames = 16088,
716716
allocations = 4317,
717717
closures = 358,
718718
variableReads = 4080,
719719
variableWrites = 1343,
720-
resets = 481,
721-
shifts = 660,
722-
resumes = 644
720+
resets = 458,
721+
shifts = 322,
722+
resumes = 306
723723
)),
724724

725725
examplesDir / "casestudies" / "inference.effekt.md" -> Some(Summary(
726726
staticDispatches = 1457444,
727727
dynamicDispatches = 3201452,
728728
patternMatches = 1474290,
729729
branches = 303298,
730-
pushedFrames = 7574480,
731-
poppedFrames = 6709185,
730+
pushedFrames = 7574476,
731+
poppedFrames = 6709181,
732732
allocations = 4626007,
733733
closures = 865541,
734734
variableReads = 2908620,
@@ -741,11 +741,11 @@ class VMTests extends munit.FunSuite {
741741
examplesDir / "pos" / "raytracer.effekt" -> Some(Summary(
742742
staticDispatches = 79696,
743743
dynamicDispatches = 0,
744-
patternMatches = 1014772,
744+
patternMatches = 795964,
745745
branches = 71995,
746746
pushedFrames = 223269,
747747
poppedFrames = 223269,
748-
allocations = 127533,
748+
allocations = 103221,
749749
closures = 0,
750750
variableReads = 77886,
751751
variableWrites = 26904,
@@ -761,26 +761,26 @@ class VMTests extends munit.FunSuite {
761761
dynamicDispatches = 0,
762762
patternMatches = 0,
763763
branches = 11,
764-
pushedFrames = 102,
765-
poppedFrames = 102,
764+
pushedFrames = 92,
765+
poppedFrames = 92,
766766
allocations = 0,
767767
closures = 0,
768768
variableReads = 61,
769769
variableWrites = 30,
770-
resets = 1,
771-
shifts = 10,
772-
resumes = 10
770+
resets = 0,
771+
shifts = 0,
772+
resumes = 0
773773
)),
774774

775775
examplesDir / "benchmarks" / "other" / "church_exponentiation.effekt" -> Some(Summary(
776776
staticDispatches = 7,
777-
dynamicDispatches = 1062912,
777+
dynamicDispatches = 797188,
778778
patternMatches = 0,
779779
branches = 5,
780780
pushedFrames = 531467,
781781
poppedFrames = 531467,
782782
allocations = 0,
783-
closures = 265750,
783+
closures = 26,
784784
variableReads = 0,
785785
variableWrites = 0,
786786
resets = 0,

effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,9 @@ object PolymorphismBoxing extends Phase[CoreTransformed, CoreTransformed] {
298298
Stmt.Alloc(id, transform(init), region, transform(body))
299299
case Stmt.Var(id, init, cap, body) =>
300300
Stmt.Var(id, transform(init), cap, transform(body))
301-
case Stmt.Reset(body) =>
302-
Stmt.Reset(transform(body))
301+
case Stmt.Reset(BlockLit(tps, cps, vps, prompt :: Nil, body)) =>
302+
Stmt.Reset(BlockLit(tps, cps, vps, prompt :: Nil, coerce(transform(body), stmt.tpe)))
303+
case Stmt.Reset(body) => ???
303304
case Stmt.Shift(prompt, body) =>
304305
Stmt.Shift(prompt, transform(body))
305306
case Stmt.Resume(k, body) =>

effekt/shared/src/main/scala/effekt/core/Tree.scala

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -272,15 +272,15 @@ enum Stmt extends Tree {
272272
case Alloc(id: Id, init: Pure, region: Id, body: Stmt)
273273

274274
// creates a fresh state handler to model local (backtrackable) state.
275-
// [[capture]] is a binding occurence.
275+
// [[capture]] is a binding occurrence.
276276
// e.g. state(init) { [x]{x: Ref} => ... }
277277
case Var(id: Id, init: Pure, capture: Id, body: Stmt)
278278
case Get(id: Id, annotatedCapt: Captures, annotatedTpe: ValueType)
279279
case Put(id: Id, annotatedCapt: Captures, value: Pure)
280280

281281
// binds a fresh prompt as [[id]] in [[body]] and delimits the scope of captured continuations
282282
// Reset({ [cap]{p: Prompt[answer] at cap} => stmt: answer}): answer
283-
case Reset(body: BlockLit)
283+
case Reset(body: Block.BlockLit)
284284

285285
// captures the continuation up to the given prompt
286286
// Invariant, it always has the shape:
@@ -703,7 +703,7 @@ object substitutions {
703703

704704
case Match(scrutinee, clauses, default) =>
705705
Match(substitute(scrutinee), clauses.map {
706-
case (id, b) => (id, substitute(b).asInstanceOf[BlockLit])
706+
case (id, b) => (id, substitute(b))
707707
}, default.map(substitute))
708708

709709
case Alloc(id, init, region, body) =>
@@ -719,11 +719,12 @@ object substitutions {
719719
case Put(id, capt, value) =>
720720
Put(substituteAsVar(id), substitute(capt), substitute(value))
721721

722+
// We annotate the answer type here since it needs to be the union of body.tpe and all shifts
722723
case Reset(body) =>
723-
Reset(substitute(body).asInstanceOf[BlockLit])
724+
Reset(substitute(body))
724725

725726
case Shift(prompt, body) =>
726-
val after = substitute(body).asInstanceOf[BlockLit]
727+
val after = substitute(body)
727728
Shift(substitute(prompt).asInstanceOf[BlockVar], after)
728729

729730
case Resume(k, body) =>

effekt/shared/src/main/scala/effekt/core/Type.scala

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -97,19 +97,21 @@ object Type {
9797
/**
9898
* Function types are the only type constructor that we have subtyping on.
9999
*/
100-
def merge(tpe1: ValueType, tpe2: ValueType, covariant: Boolean): ValueType = (tpe1, tpe2) match {
101-
case (ValueType.Boxed(btpe1, capt1), ValueType.Boxed(btpe2, capt2)) =>
100+
def merge(tpe1: ValueType, tpe2: ValueType, covariant: Boolean): ValueType = (tpe1, tpe2, covariant) match {
101+
case (tpe1, tpe2, covariant) if tpe1 == tpe2 => tpe1
102+
case (ValueType.Boxed(btpe1, capt1), ValueType.Boxed(btpe2, capt2), covariant) =>
102103
ValueType.Boxed(merge(btpe1, btpe2, covariant), merge(capt1, capt2, covariant))
103-
case (tpe1, tpe2) if covariant =>
104-
if (isSubtype(tpe1, tpe2)) tpe2 else tpe1
105-
case (tpe1, tpe2) if !covariant =>
106-
if (isSubtype(tpe1, tpe2)) tpe1 else tpe2
104+
case (TBottom, tpe2, true) => tpe2
105+
case (tpe1, TBottom, true) => tpe1
106+
case (TTop, tpe2, true) => TTop
107+
case (tpe1, TTop, true) => TTop
108+
case (TBottom, tpe2, false) => TBottom
109+
case (tpe1, TBottom, false) => TBottom
110+
case (TTop, tpe2, false) => tpe2
111+
case (tpe1, TTop, false) => tpe1
112+
// TODO this swallows a lot of bugs that we NEED to fix
107113
case _ => tpe1
108-
}
109-
private def isSubtype(tpe1: ValueType, tpe2: ValueType): Boolean = (tpe1, tpe2) match {
110-
case (tpe1, TTop) => true
111-
case (TBottom, tpe1) => true
112-
case _ => false // conservative :)
114+
// sys error s"Cannot compare ${tpe1} ${tpe2} in ${covariant}" // conservative :)
113115
}
114116

115117
def merge(tpe1: BlockType, tpe2: BlockType, covariant: Boolean): BlockType = (tpe1, tpe2) match {
@@ -129,12 +131,12 @@ object Type {
129131
assert(targs.size == tparams.size, "Wrong number of type arguments")
130132
assert(cargs.size == cparams.size, "Wrong number of capture arguments")
131133

132-
val vsubst = (tparams zip targs).toMap
134+
val tsubst = (tparams zip targs).toMap
133135
val csubst = (cparams zip cargs).toMap
134136
BlockType.Function(Nil, Nil,
135-
vparams.map { tpe => substitute(tpe, vsubst, Map.empty) },
136-
bparams.map { tpe => substitute(tpe, vsubst, Map.empty) },
137-
substitute(result, vsubst, csubst))
137+
vparams.map { tpe => substitute(tpe, tsubst, Map.empty) },
138+
bparams.map { tpe => substitute(tpe, tsubst, Map.empty) },
139+
substitute(result, tsubst, csubst))
138140
}
139141

140142
def substitute(capt: Captures, csubst: Map[Id, Captures]): Captures = capt.flatMap {
@@ -206,7 +208,11 @@ object Type {
206208
case Stmt.Var(id, init, cap, body) => body.tpe
207209
case Stmt.Get(id, capt, tpe) => tpe
208210
case Stmt.Put(id, capt, value) => TUnit
209-
case Stmt.Reset(body) => body.returnType
211+
case Stmt.Reset(BlockLit(_, _, _, prompt :: Nil, body)) => prompt.tpe match {
212+
case TPrompt(tpe) => tpe
213+
case _ => ???
214+
}
215+
case Stmt.Reset(body) => ???
210216
case Stmt.Shift(prompt, body) => body.bparams match {
211217
case core.BlockParam(id, BlockType.Interface(ResumeSymbol, List(result, answer)), captures) :: Nil => result
212218
case _ => ???

effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ object Normalizer { normal =>
3535
exprs: Map[Id, Expr],
3636
decls: DeclarationContext, // for field selection
3737
usage: mutable.Map[Id, Usage], // mutable in order to add new information after renaming
38-
maxInlineSize: Int // to control inlining and avoid code bloat
38+
maxInlineSize: Int, // to control inlining and avoid code bloat
39+
preserveBoxing: Boolean // for LLVM, prevents some optimizations
3940
) {
4041
def bind(id: Id, expr: Expr): Context = copy(exprs = exprs + (id -> expr))
4142
def bind(id: Id, block: Block): Context = copy(blocks = blocks + (id -> block))
@@ -65,14 +66,14 @@ object Normalizer { normal =>
6566
case None => false
6667
}
6768

68-
def normalize(entrypoints: Set[Id], m: ModuleDecl, maxInlineSize: Int): ModuleDecl = {
69+
def normalize(entrypoints: Set[Id], m: ModuleDecl, maxInlineSize: Int, preserveBoxing: Boolean): ModuleDecl = {
6970
// usage information is used to detect recursive functions (and not inline them)
7071
val usage = Reachable(entrypoints, m)
7172

7273
val defs = m.definitions.collect {
7374
case Toplevel.Def(id, block) => id -> block
7475
}.toMap
75-
val context = Context(defs, Map.empty, DeclarationContext(m.declarations, m.externs), mutable.Map.from(usage), maxInlineSize)
76+
val context = Context(defs, Map.empty, DeclarationContext(m.declarations, m.externs), mutable.Map.from(usage), maxInlineSize, preserveBoxing)
7677

7778
val (normalizedDefs, _) = normalizeToplevel(m.definitions)(using context)
7879
m.copy(definitions = normalizedDefs)
@@ -138,10 +139,10 @@ object Normalizer { normal =>
138139

139140
// TODO for `New` we should track how often each operation is used, not the object itself
140141
// to decide inlining.
141-
private def shouldInline(b: BlockLit, boundBy: Option[BlockVar])(using C: Context): Boolean = boundBy match {
142+
private def shouldInline(b: BlockLit, boundBy: Option[BlockVar], blockArgs: List[Block])(using C: Context): Boolean = boundBy match {
142143
case Some(id) if isRecursive(id.id) => false
143144
case Some(id) => isOnce(id.id) || b.body.size <= C.maxInlineSize
144-
case None => true
145+
case _ => blockArgs.exists { b => b.isInstanceOf[BlockLit] } // higher-order function with known arg
145146
}
146147

147148
private def active(e: Expr)(using Context): Expr =
@@ -171,7 +172,7 @@ object Normalizer { normal =>
171172
// -------
172173
case Stmt.App(b, targs, vargs, bargs) =>
173174
active(b) match {
174-
case NormalizedBlock.Known(b: BlockLit, boundBy) if shouldInline(b, boundBy) =>
175+
case NormalizedBlock.Known(b: BlockLit, boundBy) if shouldInline(b, boundBy, bargs) =>
175176
reduce(b, targs, vargs.map(normalize), bargs.map(normalize))
176177
case normalized =>
177178
Stmt.App(normalized.shared, targs, vargs.map(normalize), bargs.map(normalize))
@@ -181,7 +182,7 @@ object Normalizer { normal =>
181182
active(b) match {
182183
case n @ NormalizedBlock.Known(Block.New(impl), boundBy) =>
183184
selectOperation(impl, method) match {
184-
case b: BlockLit if shouldInline(b, boundBy) => reduce(b, targs, vargs.map(normalize), bargs.map(normalize))
185+
case b: BlockLit if shouldInline(b, boundBy, bargs) => reduce(b, targs, vargs.map(normalize), bargs.map(normalize))
185186
case _ => Stmt.Invoke(n.shared, method, methodTpe, targs, vargs.map(normalize), bargs.map(normalize))
186187
}
187188

@@ -213,6 +214,14 @@ object Normalizer { normal =>
213214

214215
def normalizeVal(id: Id, tpe: ValueType, binding: Stmt, body: Stmt): Stmt = normalize(binding) match {
215216

217+
// [[ val x = ABORT; body ]] = ABORT
218+
case abort if !C.preserveBoxing && abort.tpe == Type.TBottom =>
219+
abort
220+
221+
case abort @ Stmt.Shift(p, BlockLit(tparams, cparams, vparams, List(k), body))
222+
if !C.preserveBoxing && !Variables.free(body).containsBlock(k.id) =>
223+
abort
224+
216225
// [[ val x = sc match { case id(ps) => body2 }; body ]] = sc match { case id(ps) => val x = body2; body }
217226
case Stmt.Match(sc, List((id2, BlockLit(tparams2, cparams2, vparams2, bparams2, body2))), None) =>
218227
Stmt.Match(sc, List((id2, BlockLit(tparams2, cparams2, vparams2, bparams2,

effekt/shared/src/main/scala/effekt/core/optimizer/Optimizer.scala

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ object Optimizer extends Phase[CoreTransformed, CoreTransformed] {
2121

2222
def optimize(source: Source, mainSymbol: symbols.Symbol, core: ModuleDecl)(using Context): ModuleDecl =
2323

24+
val isLLVM = Context.config.backend().name == "llvm"
25+
2426
var tree = core
2527

2628
// (1) first thing we do is simply remove unused definitions (this speeds up all following analysis and rewrites)
@@ -37,18 +39,16 @@ object Optimizer extends Phase[CoreTransformed, CoreTransformed] {
3739

3840
def normalize(m: ModuleDecl) = {
3941
val anfed = BindSubexpressions.transform(m)
40-
val normalized = Normalizer.normalize(Set(mainSymbol), anfed, Context.config.maxInlineSize().toInt)
41-
Deadcode.remove(mainSymbol, normalized)
42+
val normalized = Normalizer.normalize(Set(mainSymbol), anfed, Context.config.maxInlineSize().toInt, isLLVM)
43+
val live = Deadcode.remove(mainSymbol, normalized)
44+
val tailRemoved = RemoveTailResumptions(live)
45+
tailRemoved
4246
}
4347

44-
// (3) normalize once and remove beta redexes
48+
// (3) normalize a few times (since tail resumptions might only surface after normalization and leave dead Resets)
4549
tree = Context.timed("normalize-1", source.name) { normalize(tree) }
46-
47-
// (4) optimize continuation capture in the tail-resumptive case
48-
tree = Context.timed("tail-resumptions", source.name) { RemoveTailResumptions(tree) }
49-
50-
// (5) normalize again to clean up and remove new redexes
5150
tree = Context.timed("normalize-2", source.name) { normalize(tree) }
51+
tree = Context.timed("normalize-3", source.name) { normalize(tree) }
5252

5353
tree
5454
}

effekt/shared/src/main/scala/effekt/core/optimizer/RemoveTailResumptions.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ object RemoveTailResumptions {
3838
case Stmt.Get(id, annotatedCapt, annotatedTpe) => false
3939
case Stmt.Put(id, annotatedCapt, value) => false
4040
case Stmt.Reset(BlockLit(tparams, cparams, vparams, bparams, body)) => tailResumptive(k, body) // is this correct?
41-
case Stmt.Shift(prompt, body) => false
41+
case Stmt.Shift(prompt, body) => stmt.tpe == Type.TBottom
4242
case Stmt.Resume(k2, body) => k2.id == k // what if k is free in body?
4343
case Stmt.Hole() => true
4444
}

0 commit comments

Comments
 (0)