Skip to content

Commit 90a7f1e

Browse files
authored
Fix canonical ordering by using the ordering at the declaration site (#859)
This tries to fix issue #849 by NOT canonicalizing AFTER unification. Instead, we canonicalize FIRST on the function type and then MAINTAIN the order at the callsite. This requires us to NOT go into type `Effect`, which is a set and thus removes duplicates and destroys the ordering... Sadly, it is very difficult to write a test for this :(
1 parent ffde209 commit 90a7f1e

File tree

8 files changed

+53
-45
lines changed

8 files changed

+53
-45
lines changed

effekt/shared/src/main/scala/effekt/Namer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ object Namer extends Phase[Parsed, NameResolved] {
699699
}
700700

701701
val effs = resolve(effects).distinct
702-
effs.canonical.foreach { eff =>
702+
CanonicalOrdering(effs.toList) foreach { eff =>
703703
val cap = CaptureParam(eff.name)
704704
cps = cps :+ cap
705705
}

effekt/shared/src/main/scala/effekt/Typer.scala

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -374,23 +374,23 @@ object Typer extends Phase[NameResolved, Typechecked] {
374374
val declaredOperation = interface.operations.find(o => o.name.name == op.name).getOrElse {
375375
Context.abort(pretty"Operation ${op.name} not defined in ${interface.name}.")
376376
}
377-
val declaredType = Context.lookupFunctionType(d.definition)
377+
val declared = Context.lookupFunctionType(d.definition)
378378

379379
def assertArity(kind: String, got: Int, expected: Int): Unit =
380380
if (got != expected)
381381
Context.abort(pretty"Number of ${kind} (${got}) does not match declaration of '${op.name}', which expects ${expected}.")
382382

383383
// if we have zero given type parameters, we synthesize them -- no need to check then
384-
if (tparams.size != 0) assertArity("type parameters", tparams.size, declaredType.tparams.size - targs.size)
385-
assertArity("value parameters", vparams.size, declaredType.vparams.size)
384+
if (tparams.nonEmpty) assertArity("type parameters", tparams.size, declared.tparams.size - targs.size)
385+
assertArity("value parameters", vparams.size, declared.vparams.size)
386386

387387
// effect E[A, B, ...] { def op[C, D, ...]() = ... } !--> op[A, B, ..., C, D, ...]
388388
// The parameters C, D, ... are existentials
389-
val existentialParams: List[TypeVar] = if (tparams.size == declaredType.tparams.size - targs.size) {
389+
val existentialParams: List[TypeVar] = if (tparams.size == declared.tparams.size - targs.size) {
390390
tparams.map { tparam => tparam.symbol.asTypeParam }
391391
} else {
392392
// using the invariant that the universals are prepended to type parameters of the operation
393-
declaredType.tparams.drop(targs.size).map { tp =>
393+
declared.tparams.drop(targs.size).map { tp =>
394394
// recreate "fresh" type variables
395395
val name = tp.name
396396
TypeVar.TypeParam(name)
@@ -400,23 +400,24 @@ object Typer extends Phase[NameResolved, Typechecked] {
400400

401401
Context.annotate(Annotations.TypeParameters, d, existentialParams)
402402

403-
val canonicalEffects = declaredType.effects.canonical
403+
// canonical ordering on annotated effects
404+
val canonical = CanonicalOrdering(declared.effects.toList)
404405

405406
// distinguish between handler operation or object operation (which does not capture a cont.)
406407
val Result(_, effs) = continuationDetails match {
407408
// normal object: no continuation there
408409
case None =>
409410
// block parameters are to be bound by the definition itself instead of by resume when using handlers
410-
assertArity("block parameters", bparams.size, declaredType.bparams.size)
411+
assertArity("block parameters", bparams.size, declared.bparams.size)
411412

412413
val cparamsForBlocks = bparams.map { p => p.symbol.capture }
413414
// will be introduced as capabilities in a later phase
414-
val cparamsForEffects = canonicalEffects.map { tpe => CaptureParam(tpe.name) }
415+
val cparamsForEffects = canonical.map { tpe => CaptureParam(tpe.name) }
415416
val cparams = cparamsForBlocks ++ cparamsForEffects
416417

417418
// substitute actual type parameter and capture parameters for declared ones
418-
val FunctionType(Nil, Nil, vps, bps, tpe, effs) =
419-
Context.instantiate(declaredType, targs ++ existentials, cparams.map(cap => CaptureSet(cap))) : @unchecked
419+
val (vps, bps, tpe, effs) =
420+
Context.instantiate(declared, targs ++ existentials, cparams.map(cap => CaptureSet(cap))) : @unchecked
420421

421422
(vparams zip vps).foreach {
422423
case (param, declaredType) =>
@@ -435,29 +436,29 @@ object Typer extends Phase[NameResolved, Typechecked] {
435436
}
436437

437438
// these capabilities are later introduced as parameters in capability passing
438-
val capabilities = (effs.canonical zip cparamsForEffects).map {
439+
val capabilities = (CanonicalOrdering(effs) zip cparamsForEffects).map {
439440
case (tpe, capt) => Context.freshCapabilityFor(tpe, CaptureSet(capt))
440441
}
441442

442443
val Result(bodyType, bodyEffs) = Context.bindingCapabilities(d, capabilities) {
443444
body checkAgainst tpe
444445
}
445-
Result(bodyType, bodyEffs -- effs)
446+
Result(bodyType, bodyEffs -- Effects(effs))
446447

447448
// handler implementation: we have a continuation
448449
case Some(ret, continuationCapt) =>
449450

450451
if (bparams.nonEmpty)
451452
Context.error("Block parameters are bound by resume and not the effect operation itself")
452453

453-
def isBidirectional = canonicalEffects.nonEmpty || declaredType.bparams.nonEmpty
454+
def isBidirectional = canonical.nonEmpty || declared.bparams.nonEmpty
454455

455456
val cparamsForBlocks = declaredOperation.bparams.map { p => CaptureParam(p.name) } // use the original name
456-
val cparamsForEffects = canonicalEffects.map { tpe => CaptureParam(tpe.name) } // use the type name
457+
val cparamsForEffects = canonical.map { tpe => CaptureParam(tpe.name) } // use the type name
457458
val cparams = cparamsForBlocks ++ cparamsForEffects
458459

459-
val FunctionType(Nil, Nil, vps, bps, tpe, effs) =
460-
Context.instantiate(declaredType, targs ++ existentials, cparams.map(cap => CaptureSet(cap))) : @unchecked
460+
val (vps, bps, tpe, effs) =
461+
Context.instantiate(declared, targs ++ existentials, cparams.map(cap => CaptureSet(cap))) : @unchecked
461462

462463
(vparams zip vps).foreach {
463464
case (param, declaredType) =>
@@ -470,7 +471,7 @@ object Typer extends Phase[NameResolved, Typechecked] {
470471
// (4) synthesize type of continuation
471472
val resumeType = if (isBidirectional) {
472473
// resume { {f} => e }
473-
val resumeType = FunctionType(Nil, cparams, Nil, bps, tpe, effs)
474+
val resumeType = FunctionType(Nil, cparams, Nil, bps, tpe, Effects(effs))
474475
val resumeCapt = CaptureParam(Name.local("resumeBlock"))
475476
FunctionType(Nil, List(resumeCapt), Nil, List(resumeType), ret, Effects.Pure)
476477
} else {
@@ -574,7 +575,7 @@ object Typer extends Phase[NameResolved, Typechecked] {
574575

575576
// (4) Compute blocktype of this constructor with rigid type vars
576577
// i.e. Cons : `(?t1, List[?t1]) => List[?t1]`
577-
val FunctionType(_, _, vps, _, ret, _) = Context.instantiate(sym.toType, targs, Nil)
578+
val (vps, _, ret, _) = Context.instantiate(sym.toType, targs, Nil)
578579

579580
// (5) given a scrutinee of `List[Int]`, we learn `?t1 -> Int`
580581
matchPattern(sc, ret, p)
@@ -1205,7 +1206,7 @@ object Typer extends Phase[NameResolved, Typechecked] {
12051206

12061207
// (1) Instantiate blocktype
12071208
// e.g. `[A, B] (A, A) => B` becomes `(?A, ?A) => ?B`
1208-
val (typeArgs, captArgs, bt @ FunctionType(_, _, vps, bps, ret, retEffs)) = Context.instantiateFresh(funTpe)
1209+
val (typeArgs, captArgs, (vps, bps, ret, retEffs)) = Context.instantiateFresh(CanonicalOrdering(funTpe))
12091210

12101211
// provided type arguments flow into the fresh unification variables (i.e., Int <: ?A)
12111212
if (targs.nonEmpty) (targs zip typeArgs).foreach { case (targ, tvar) => matchExpected(tvar, targ) }
@@ -1240,22 +1241,16 @@ object Typer extends Phase[NameResolved, Typechecked] {
12401241

12411242
// We add return effects last to have more information at this point to
12421243
// concretize the effect.
1243-
effs = effs ++ retEffs
1244+
effs = effs ++ Effects(retEffs)
12441245

12451246
// annotate call node with inferred type arguments
12461247
Context.annotateTypeArgs(call, typeArgs)
12471248

12481249
// Annotate the call target tree with the additional capabilities
1249-
// We need to establish the canonical ordering of capabilities.
1250-
// 1) we have to use the capabilities, which are annotated on the original function type
1251-
// 2) we need to dealias
1252-
// 3) we need to compute distinct effects on the dealiased list
1253-
// 3) and only then substitute
1254-
//
1255-
// This is important since
1256-
// [A, B](): Unit / { State[A], State[B] }
1257-
// with A := Int and B := Int requires us to pass two capabilities.
1258-
val capabilities = Context.provideCapabilities(call, retEffs.canonical.map(Context.unification.apply))
1250+
// The canonical ordering of capabilities is defined by the ordering of the definition site,
1251+
// not the callsite, so we need to be careful to not use `distinct` on `retEffs`
1252+
// since this might conflate State[A] and State[B] after A -> Int, B -> Int.
1253+
val capabilities = Context.provideCapabilities(call, retEffs.map(Context.unification.apply))
12591254

12601255
val captParams = captArgs.drop(bargs.size)
12611256
(captParams zip capabilities) foreach { case (param, cap) =>

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,7 @@ object Transformer extends Phase[Typechecked, CoreTransformed] {
738738
// TODO this is exactly like in [[Callable.toType]] -- TODO repeated here:
739739
// TODO currently the return type cannot refer to the annotated effects, so we can make up capabilities
740740
// in the future namer needs to annotate the function with the capture parameters it introduced.
741-
val cparams = bparams.map(b => b.capture) ++ effects.canonical.map { tpe => symbols.CaptureParam(tpe.name) }
741+
val cparams = bparams.map(b => b.capture) ++ CanonicalOrdering(effects.toList).map { tpe => symbols.CaptureParam(tpe.name) }
742742
val vparamTpes = vparams.map(t => substitution.substitute(t.tpe.getOrElse {
743743
INTERNAL_ERROR("Operation value parameters should have an annotated type.")
744744
}))
@@ -756,7 +756,7 @@ object Transformer extends Phase[Typechecked, CoreTransformed] {
756756
def operationAtDeclaration(tparamsInterface: List[Id], op: symbols.Operation)(using Context): core.BlockType = op match {
757757
case symbols.Operation(name, tps, vps, bps, resultType, effects, interface) =>
758758
// like in asSeenFrom we need to make up cparams, they cannot occur free in the result type
759-
val capabilities = effects.canonical
759+
val capabilities = CanonicalOrdering(effects.toList)
760760
val tparams = tps.drop(tparamsInterface.size)
761761
val bparamsBlocks = bps.map(b => transform(b.tpe.getOrElse {
762762
INTERNAL_ERROR("Interface declarations should have annotated types.")
@@ -817,7 +817,7 @@ object Transformer extends Phase[Typechecked, CoreTransformed] {
817817
def transform(tpe: BlockType)(using Context): core.BlockType = tpe match {
818818
case BlockType.FunctionType(tparams, cparams, vparams, bparams, result, effects) =>
819819

820-
val capabilityTypes = effects.canonical.map(transform)
820+
val capabilityTypes = CanonicalOrdering(effects.toList).map(transform)
821821
val allBlockParams = bparams.map(transform) ++ capabilityTypes
822822

823823
assert(cparams.size == allBlockParams.size,

effekt/shared/src/main/scala/effekt/symbols/kinds.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ package object kinds {
3737
def wellformed(b: FunctionType)(using C: Context): Unit = b match {
3838
case FunctionType(tps, cps, vps, bps, res, effs) =>
3939
// TODO we could also check whether the same type variable shows up twice
40-
if (cps.size != (bps.size + effs.canonical.size)) {
40+
if (cps.size != (bps.size + effs.distinct.size)) {
4141
C.panic(s"Compiler invariant violated: different size of capture parameters and block parameters: ${b}")
4242
}
4343
vps.foreach { tpe => wellformed(tpe) }

effekt/shared/src/main/scala/effekt/symbols/symbols.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ trait Callable extends BlockSymbol {
152152
effects = effs.distinct
153153
// TODO currently the return type cannot refer to the annotated effects, so we can make up capabilities
154154
// in the future namer needs to annotate the function with the capture parameters it introduced.
155-
capt = effects.canonical.map { tpe => CaptureParam(tpe.name) }
155+
capt = CanonicalOrdering(effects.toList).map { tpe => CaptureParam(tpe.name) }
156156
} yield toType(ret, effects, capt)
157157
}
158158

effekt/shared/src/main/scala/effekt/symbols/types.scala

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ extension (i: BlockType.InterfaceType) {
6969
}
7070

7171
/**
72-
* Represents effect sets on function types.
72+
* Represents effect _sets_ (no order, no duplicates) on function types.
7373
*
7474
* All effects are dealiased by namer. Effects are inferred via [[typer.ConcreteEffects]] so
7575
* by construction all entries in the set of effects here should be concrete (no unification variables).
@@ -96,7 +96,7 @@ case class Effects(effects: List[BlockType.InterfaceType]) {
9696
def forall(p: InterfaceType => Boolean): Boolean = effects.forall(p)
9797
def exists(p: InterfaceType => Boolean): Boolean = effects.exists(p)
9898

99-
lazy val canonical: List[InterfaceType] = effects.sorted(using CanonicalOrdering)
99+
def size: Int = effects.size
100100

101101
def distinct: Effects = Effects(effects.distinct)
102102
}
@@ -122,6 +122,8 @@ object CanonicalOrdering extends Ordering[InterfaceType] {
122122
def compare(tpe1: InterfaceType, tpe2: InterfaceType): Int = compareStructural(tpe1, tpe2)
123123

124124
def compareStructural(tpe1: Any, tpe2: Any): Int = (tpe1, tpe2) match {
125+
case (_: UnificationVar, _) | (_, _: UnificationVar) =>
126+
effekt.util.messages.INTERNAL_ERROR("Cannot compute canonical ordering, since type still contains unification variables.")
125127
case (sym1: Symbol, sym2: Symbol) =>
126128
sym1.id - sym2.id
127129
case (p1: Product, p2: Product) if p1.getClass == p2.getClass =>
@@ -133,6 +135,15 @@ object CanonicalOrdering extends Ordering[InterfaceType] {
133135
}
134136

135137
def fallback(tpe1: Any, tpe2: Any): Int = tpe1.hashCode - tpe2.hashCode
138+
139+
def apply(l: List[InterfaceType]): List[InterfaceType] = l.sorted(using this)
140+
141+
def apply(f: FunctionType): FunctionType = f match {
142+
case FunctionType(tparams, cparams, vparams, bparams, result, effects) =>
143+
val (cparamsBlocks, cparamsEffects) = cparams.splitAt(bparams.size)
144+
val (cparams2, effects2) = (cparamsEffects zip effects.toList).sortBy(_._2)(using this).unzip
145+
FunctionType(tparams, cparamsBlocks ++ cparams2, vparams, bparams, result, Effects(effects2))
146+
}
136147
}
137148

138149

effekt/shared/src/main/scala/effekt/typer/ConcreteEffects.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import effekt.util.messages.ErrorMessageReifier
1010
* All effects inferred by Typer are required to be concrete and dealiased.
1111
*
1212
* This way, we can easily compare them for equality.
13+
*
14+
* This is a _set_ of effects and does not guarantee order!
1315
*/
1416
class ConcreteEffects private[typer] (protected val effects: List[InterfaceType]) {
1517

effekt/shared/src/main/scala/effekt/typer/Unification.scala

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,16 +188,16 @@ class Unification(using C: ErrorReporter) extends TypeUnifier, TypeMerger, TypeI
188188
/**
189189
* Instantiate a typescheme with provided type and capture arguments.
190190
*/
191-
def instantiate(tpe: FunctionType, targs: List[ValueType], cargs: List[Captures]): FunctionType = {
191+
def instantiate(tpe: FunctionType, targs: List[ValueType], cargs: List[Captures]): (List[ValueType], List[BlockType], ValueType, List[InterfaceType]) = {
192192
val position = C.focus
193193
val FunctionType(tparams, cparams, vparams, bparams, ret, eff) = substitution.substitute(tpe)
194194

195195
assert(targs.size == tparams.size,
196196
pp"Type argument and parameter size mismatch: ${targs.size} vs ${tparams.size} ($targs, $tparams)")
197197
assert(cargs.size == cparams.size,
198198
pp"Capture arguments and parameter size mismatch: ${cargs.size} vs ${cparams.size} ($cargs, $cparams)")
199-
assert(cparams.size == (bparams.size + eff.canonical.size),
200-
pp"Capture param count ${cparams.size} is not equal to bparam ${bparams.size} + controleffects ${eff.canonical.size}.\n ${tpe}")
199+
assert(cparams.size == (bparams.size + eff.distinct.size),
200+
pp"Capture param count ${cparams.size} is not equal to bparam ${bparams.size} + controleffects ${eff.size}.\n ${tpe}")
201201

202202
given Instantiation = Instantiation((tparams zip targs).toMap, (cparams zip cargs).toMap)
203203

@@ -207,15 +207,15 @@ class Unification(using C: ErrorReporter) extends TypeUnifier, TypeMerger, TypeI
207207

208208
val substitutedEffects = instantiate(eff)
209209

210-
FunctionType(Nil, Nil, substitutedVparams, substitutedBparams, substitutedReturn, substitutedEffects)
210+
(substitutedVparams, substitutedBparams, substitutedReturn, substitutedEffects)
211211
}
212212

213213
/**
214214
* Instantiate a typescheme with fresh, rigid type variables
215215
*
216216
* i.e. `[T1, T2, C] (T1, T1) {sigma} => T2` becomes `(?T1, ?T1){} => ?T2[C !-> ?C]`
217217
*/
218-
def instantiateFresh(tpe: FunctionType): (List[ValueType], List[Captures], FunctionType) = {
218+
def instantiateFresh(tpe: FunctionType): (List[ValueType], List[Captures], (List[ValueType], List[BlockType], ValueType, List[InterfaceType])) = {
219219
val position = C.focus
220220
val FunctionType(tparams, cparams, vparams, bparams, ret, eff) = substitution.substitute(tpe)
221221

@@ -341,7 +341,7 @@ trait TypeInstantiator { self: Unification =>
341341
BoxedType(instantiate(tpe), instantiate(capt))
342342
}
343343

344-
def instantiate(t: Effects)(using Instantiation): Effects = Effects(t.toList.map(instantiate))
344+
def instantiate(t: Effects)(using Instantiation): List[InterfaceType] = t.toList.map(instantiate)
345345

346346
def instantiate(t: BlockType)(using Instantiation): BlockType = t match {
347347
case e: InterfaceType => instantiate(e)
@@ -362,6 +362,6 @@ trait TypeInstantiator { self: Unification =>
362362
vps map instantiate,
363363
bps map instantiate,
364364
instantiate(ret),
365-
instantiate(eff))
365+
Effects(instantiate(eff)))
366366
}
367367
}

0 commit comments

Comments
 (0)