Skip to content

Commit 2867630

Browse files
Add initial theory of strings
1 parent 5efd3db commit 2867630

File tree

5 files changed

+120
-46
lines changed

5 files changed

+120
-46
lines changed

effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/NewNormalizer.scala

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ class NewNormalizer {
183183
env.lookupValue(id)
184184

185185
case core.Expr.Literal(value, annotatedType) => value match {
186-
case As.IntExpr(x) => scope.allocate("x", Value.Integer(x))
186+
case As.IntRep(x) => scope.allocate("x", Value.Integer(x))
187187
case _ => scope.allocate("x", Value.Literal(value, annotatedType))
188188
}
189189

@@ -616,7 +616,13 @@ class NewNormalizer {
616616
case Value.Make(data, tag, targs, vargs) => Expr.Make(data, tag, targs, vargs.map(embedExpr))
617617
case Value.Box(body, annotatedCapture) => Expr.Box(embedBlock(body), annotatedCapture)
618618
case Value.Var(id, annotatedType) => Expr.ValueVar(id, annotatedType)
619-
case Value.Integer(value) => theories.integers.reify(value, cx.builtinBlockVars)
619+
case Value.Integer(value) => theories.integers.reify(value, cx.builtinBlockVars, embedNeutral)
620+
case Value.String(value) => theories.strings.reify(value, cx.builtinBlockVars, embedNeutral)
621+
}
622+
623+
def embedNeutral(neutral: Neutral)(using G: TypingContext): core.Expr = neutral match {
624+
case Value.Var(id, annotatedType) => Expr.ValueVar(id, annotatedType)
625+
case Value.Extern(callee, targs, vargs) => Expr.PureApp(callee, targs, vargs.map(embedExpr))
620626
}
621627

622628
def embedExpr(addr: Addr)(using G: TypingContext): core.Expr = Expr.ValueVar(addr, G.lookupValue(addr))

effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/builtins.scala

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ lazy val integers: Builtins = Map(
3333
// Integer arithmetic operations with symbolic simplification support
3434
// ----------
3535
builtin("effekt::infixAdd(Int, Int)") {
36-
case As.IntExpr(x) :: As.IntExpr(y) :: Nil => semantics.Value.Integer(theories.integers.add(x, y))
36+
case As.IntRep(x) :: As.IntRep(y) :: Nil => semantics.Value.Integer(theories.integers.add(x, y))
3737
},
3838
builtin("effekt::infixSub(Int, Int)") {
39-
case As.IntExpr(x) :: As.IntExpr(y) :: Nil => semantics.Value.Integer(theories.integers.sub(x, y))
39+
case As.IntRep(x) :: As.IntRep(y) :: Nil => semantics.Value.Integer(theories.integers.sub(x, y))
4040
},
4141
builtin("effekt::infixMul(Int, Int)") {
42-
case As.IntExpr(x) :: As.IntExpr(y) :: Nil => semantics.Value.Integer(theories.integers.mul(x, y))
42+
case As.IntRep(x) :: As.IntRep(y) :: Nil => semantics.Value.Integer(theories.integers.mul(x, y))
4343
},
4444
// Integer arithmetic operations only evaluated for literals
4545
// ----------
@@ -190,7 +190,7 @@ lazy val booleans: Builtins = Map(
190190

191191
lazy val strings: Builtins = Map(
192192
builtin("effekt::infixConcat(String, String)") {
193-
case As.String(x) :: As.String(y) :: Nil => x + y
193+
case As.StringRep(x) :: As.StringRep(y) :: Nil => semantics.Value.String(theories.strings.concat(x, y))
194194
},
195195

196196
builtin("effekt::infixEq(String, String)") {
@@ -246,18 +246,19 @@ protected object As {
246246
case semantics.Value.Literal(value: scala.Long, _) => Some(value)
247247
case semantics.Value.Literal(value: scala.Int, _) => Some(value.toLong)
248248
case semantics.Value.Literal(value: java.lang.Integer, _) => Some(value.toLong)
249+
case semantics.Value.Integer(value) if value.isLiteral => Some(value.value)
249250
case _ => None
250251
}
251252
}
252253

253-
object IntExpr {
254-
def unapply(v: semantics.Value): Option[theories.integers.Integer] = v match {
254+
object IntRep {
255+
def unapply(v: semantics.Value): Option[theories.integers.IntegerRep] = v match {
255256
// Integer literals not yet embedded into the theory of integers
256257
case semantics.Value.Literal(value: scala.Long, _) => Some(theories.integers.embed(value))
257258
case semantics.Value.Literal(value: scala.Int, _) => Some(theories.integers.embed(value.toLong))
258259
case semantics.Value.Literal(value: java.lang.Integer, _) => Some(theories.integers.embed(value.toLong))
259-
// Variables of type integer
260-
case semantics.Value.Var(id, tpe) if tpe == Type.TInt => Some(theories.integers.embed(id))
260+
// Neutrals (e.g. variables or extern calls)
261+
case n: semantics.Neutral => Some(theories.integers.embed(n))
261262
// Already embedded integers
262263
case semantics.Value.Integer(value) => Some(value)
263264
case _ => None
@@ -274,6 +275,16 @@ protected object As {
274275
object String {
275276
def unapply(v: semantics.Value): Option[java.lang.String] = v match {
276277
case semantics.Value.Literal(value: java.lang.String, _) => Some(value)
278+
case semantics.Value.String(value) if value.isLiteral => Some(value.value.head.asInstanceOf[java.lang.String])
279+
case _ => None
280+
}
281+
}
282+
283+
object StringRep {
284+
def unapply(v: semantics.Value): Option[theories.strings.StringRep] = v match {
285+
case semantics.Value.Literal(value: java.lang.String, _) => Some(theories.strings.embed(value))
286+
case n: semantics.Neutral => Some(theories.strings.embed(n))
287+
case semantics.Value.String(value) => Some(value)
277288
case _ => None
278289
}
279290
}

effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/semantics.scala

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@ object semantics {
2424
type Variables = Set[Id]
2525
def all[A](ts: List[A], f: A => Variables): Variables = ts.flatMap(f).toSet
2626

27+
type Neutral = Value.Var | Value.Extern
28+
2729
enum Value {
2830
// Stuck (neutrals)
2931
case Var(id: Id, annotatedType: ValueType)
3032
case Extern(f: BlockVar, targs: List[ValueType], vargs: List[Addr])
3133

3234
// Values with specialized representation for algebraic simplification
33-
case Integer(value: theories.integers.Integer)
35+
case Integer(value: theories.integers.IntegerRep)
36+
case String(value: theories.strings.StringRep)
3437

3538
// Fallback literal for other values types without special representation
3639
case Literal(value: Any, annotatedType: ValueType)
@@ -47,6 +50,7 @@ object semantics {
4750
case Value.Extern(id, targs, vargs) => vargs.toSet
4851
case Value.Literal(value, annotatedType) => Set.empty
4952
case Value.Integer(value) => value.free
53+
case Value.String(value) => value.free
5054
case Value.Make(data, tag, targs, vargs) => vargs.toSet
5155
// Box abstracts over all free computation variables, only when unboxing, they occur free again
5256
case Value.Box(body, tpe) => body.free
@@ -680,6 +684,8 @@ object semantics {
680684
parens(hsep(vargs.map(toDoc), comma))
681685

682686
case Value.Literal(value, _) => util.show(value)
687+
case Value.Integer(value) => value.show
688+
case Value.String(value) => value.show
683689

684690
case Value.Make(data, tag, targs, vargs) =>
685691
"make" <+> toDoc(data) <+> toDoc(tag) <>
@@ -690,8 +696,6 @@ object semantics {
690696
"box" <+> braces(nest(line <> toDoc(body) <> line))
691697

692698
case Value.Var(id, tpe) => toDoc(id)
693-
694-
case Value.Integer(value) => value.show
695699
}
696700

697701
def toDoc(block: Block): Doc = block match {

effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/theories/integers.scala

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,26 @@ package effekt.core.optimizer.normalizer.theories
22

33
import effekt.core.Block.BlockVar
44
import effekt.core.{Expr, Id, Type}
5+
import effekt.core.optimizer.normalizer.semantics.Neutral
56

67
/**
78
* Theory for integers with neutral variables: multivariate Laurent polynomials with 64-bit signed integer coefficients.
9+
* Compare https://en.wikipedia.org/wiki/Laurent_polynomial
810
*
911
* KNOWN LIMITATION: This implementation assumes 64-bit signed integers.
1012
* Unfortunately, this is unsound for the JavaScript backend, which uses JavaScript numbers that are IEEE-754 doubles.
1113
*/
1214
object integers {
13-
case class Integer(value: Long, addends: Addends) {
14-
val free: Set[Id] = addends.flatMap { case (factors, _) => factors.keys }.toSet
15-
15+
case class IntegerRep(value: Long, addends: Addends) {
16+
val free: Set[Id] = addends.flatMap { case (factors, _) => factors.keys.flatMap(_.free) }.toSet
17+
def isLiteral: Boolean = addends.isEmpty
1618
def show: String = {
17-
val Integer(v, a) = this
19+
val IntegerRep(v, a) = this
1820
val terms = a.map { case (factors, n) =>
1921
val factorStr = if (factors.isEmpty) "1" else {
20-
factors.map { case (id, exp) =>
21-
if (exp == 1) s"${id.name}"
22-
else s"${id.name}^$exp"
22+
factors.map { case (n, exp) =>
23+
if (exp == 1) "<neutral>"
24+
else s"<neutral>^$exp"
2325
}.mkString("*")
2426
}
2527
if (n == 1) s"$factorStr"
@@ -36,24 +38,25 @@ object integers {
3638
}
3739
import Operation._
3840

39-
def embed(value: Long): integers.Integer = Integer(value, Map.empty)
40-
def embed(id: Id): integers.Integer = Integer(0, Map(Map(id -> 1) -> 1))
41+
def embed(value: Long): integers.IntegerRep = IntegerRep(value, Map.empty)
42+
def embed(n: Neutral): integers.IntegerRep = IntegerRep(0, Map(Map(n -> 1) -> 1))
4143

42-
def reify(value: Integer, embedBuiltinName: String => BlockVar): Expr = Reify(embedBuiltinName).reify(value)
44+
def reify(value: IntegerRep, embedBuiltinName: String => BlockVar, embedNeutral: Neutral => Expr): Expr =
45+
Reify(embedBuiltinName, embedNeutral).reify(value)
4346

4447
// 3 * x * x / y = Addend(3, Map(x -> 2, y -> -1))
4548
type Addends = Map[Factors, Long]
46-
type Factors = Map[Id, Int]
49+
type Factors = Map[Neutral, Int]
4750

48-
def normalize(n: Integer): Integer = normalized(n.value, n.addends)
51+
def normalize(n: IntegerRep): IntegerRep = normalized(n.value, n.addends)
4952

50-
def normalized(value: Long, addends: Addends): Integer =
53+
def normalized(value: Long, addends: Addends): IntegerRep =
5154
val (const, norm) = normalizeAddends(addends)
52-
Integer(value + const, norm)
55+
IntegerRep(value + const, norm)
5356

54-
def add(l: Integer, r: Integer): Integer = (l, r) match {
57+
def add(l: IntegerRep, r: IntegerRep): IntegerRep = (l, r) match {
5558
// 2 + (3 * x) + 4 + (5 * y) = 6 + (3 * x) + (5 * y)
56-
case (Integer(x, xs), Integer(y, ys)) =>
59+
case (IntegerRep(x, xs), IntegerRep(y, ys)) =>
5760
normalized(x + y, add(xs, ys))
5861
}
5962

@@ -81,20 +84,20 @@ object integers {
8184
(constant, filtered)
8285
}
8386

84-
def neg(l: Integer): Integer = mul(l, -1)
87+
def neg(l: IntegerRep): IntegerRep = mul(l, -1)
8588

8689
// (42 + 3*x + y) - (42 + 3*x + y) = (42 + 3*x + y) + (-1*42 + -1*3*x + -1*y)
87-
def sub(l: Integer, r: Integer): Integer =
90+
def sub(l: IntegerRep, r: IntegerRep): IntegerRep =
8891
add(l, neg(r))
8992

90-
def mul(l: Integer, factor: Long): Integer = l match {
91-
case Integer(value, addends) =>
92-
Integer(value * factor, addends.map { case (f, n) => f -> n * factor })
93+
def mul(l: IntegerRep, factor: Long): IntegerRep = l match {
94+
case IntegerRep(value, addends) =>
95+
IntegerRep(value * factor, addends.map { case (f, n) => f -> n * factor })
9396
}
9497

95-
def mul(l: Integer, factor: Factors): Integer = l match {
96-
case Integer(value, addends) =>
97-
Integer(0, Map(factor -> value) ++ addends.map { case (f, n) =>
98+
def mul(l: IntegerRep, factor: Factors): IntegerRep = l match {
99+
case IntegerRep(value, addends) =>
100+
IntegerRep(0, Map(factor -> value) ++ addends.map { case (f, n) =>
98101
mul(f, factor) -> n
99102
})
100103
}
@@ -111,20 +114,20 @@ object integers {
111114

112115
// x1^2 * x2^0 * x3^3 = x1^2 * x3^3
113116
def normalizeFactors(f: Factors): Factors =
114-
f.filterNot { case (id, exp) => exp == 0 }
117+
f.filterNot { case (n, exp) => exp == 0 }
115118

116119
// (42 + 3*x + y) * (42 + 3*x + y)
117120
// =
118121
// (42 + 3*x + y) * 42 + (42 + 3*x + y) * 3*x + (42 + 3*x + y) * y
119-
def mul(l: Integer, r: Integer): Integer = r match {
120-
case Integer(y, ys) =>
121-
var sum: Integer = mul(l, y)
122+
def mul(l: IntegerRep, r: IntegerRep): IntegerRep = r match {
123+
case IntegerRep(y, ys) =>
124+
var sum: IntegerRep = mul(l, y)
122125
ys.foreach { case (f, n) => sum = add(sum, mul(mul(l, n), f)) }
123126
normalize(sum)
124127
}
125128

126-
case class Reify(embedBuiltinName: String => BlockVar) {
127-
def reifyVar(id: Id): Expr = Expr.ValueVar(id, Type.TInt)
129+
case class Reify(embedBuiltinName: String => BlockVar, embedNeutral: Neutral => Expr) {
130+
def reifyVar(n: Neutral): Expr = embedNeutral(n)
128131

129132
def reifyInt(v: Long): Expr = Expr.Literal(v, Type.TInt)
130133

@@ -135,8 +138,8 @@ object integers {
135138
case Div => Expr.PureApp(embedBuiltinName("effekt::infixDiv(Int, Int)"), List(), List(l, r))
136139
}
137140

138-
def reify(v: Integer): Expr =
139-
val Integer(const, addends) = normalize(v)
141+
def reify(v: IntegerRep): Expr =
142+
val IntegerRep(const, addends) = normalize(v)
140143

141144
val adds = addends.toList.map { case (factors, n) =>
142145
if (n == 1) reifyFactors(factors)
@@ -150,7 +153,7 @@ object integers {
150153
reifyInt(const)
151154
}
152155

153-
def reifyFactor(x: Id, n: Int): Expr =
156+
def reifyFactor(x: Neutral, n: Int): Expr =
154157
if (n <= 0) sys error "Should not happen"
155158
else if (n == 1) reifyVar(x)
156159
else reifyOp(reifyVar(x), Mul, reifyFactor(x, n - 1))
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package effekt.core.optimizer.normalizer.theories
2+
3+
import effekt.core.Block.BlockVar
4+
import effekt.core.{Expr, Id, Type}
5+
import effekt.core.optimizer.normalizer.semantics.Neutral
6+
7+
/**
8+
* Theory for strings: free extension of the string monoid
9+
* Compare https://arxiv.org/pdf/2306.15375
10+
*
11+
* Invariant: there are no adjacent literals in a StringRep
12+
*/
13+
object strings {
14+
case class StringRep(value: List[String | Neutral]) {
15+
val free: Set[Id] = value.collect { case n: Neutral => n.free }.flatten.toSet
16+
def isLiteral: Boolean = value.length == 1 && value.head.isInstanceOf[String]
17+
def show: String = {
18+
val terms = value.map {
19+
case s: String => s""""$s""""
20+
case n: Neutral => "<neutral>"
21+
}
22+
terms.mkString(" ++ ")
23+
}
24+
}
25+
26+
def embed(value: String): StringRep = StringRep(List(value))
27+
def embed(value: Neutral): StringRep = StringRep(List(value))
28+
29+
def reify(value: StringRep, embedBuiltinName: String => BlockVar, embedNeutral: Neutral => Expr): Expr = value match {
30+
case StringRep(parts) =>
31+
parts.map {
32+
case s: String => Expr.Literal(s, Type.TString)
33+
case n: Neutral => embedNeutral(n)
34+
}.reduceLeft { (l, r) =>
35+
Expr.PureApp(embedBuiltinName("effekt::infixConcat(String, String)"), List(), List(l, r))
36+
}
37+
}
38+
39+
def concat(l: StringRep, r: StringRep): StringRep = (l, r) match {
40+
case (StringRep(xs), StringRep(ys)) =>
41+
(xs, ys) match {
42+
// fuse trailing / leading string literals at the boundary (if any)
43+
case (init :+ (s1: String), (s2: String) :: tail) =>
44+
val concatenated: String | Neutral = s1 + s2
45+
StringRep((init :+ concatenated) ::: tail)
46+
case _ =>
47+
StringRep(xs ::: ys)
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)