Skip to content

Commit 6b2b191

Browse files
dvdvgtjiribenesmarvinbornerphischub-studios
authored
Add basic string interpolation / string templates (#743)
Fixes #722 and adds basic support for string templates. There are a few caveats to keep in mind with the current initial implementation: - All arguments already have to be strings. It is basically just syntactic sugar so that you can write `"${a.show} and ${b.show}"` instead of `a.show ++ " and " ++ b.show` where `a` and `b` are some 'showable' values. Internally, however, this is just how it is desugared. - String templates cannot be used to match string literals in pattern matches - The generated JS code looks a bit weird: ``` const v_r_897 = $effekt.println(((((((((("GET ") + (domain_0)))) + (((("/users/") + (user_0))))))) + (((("/resource/") + (('' + resourceId_0)))))))); ``` + If one forgets to call `show` on arguments which are not already of type string, the error messages are hideous and possibly confusing: <details> <summary>error</summary> ``` -There are multiple overloads, which all fail to check: -Possible overload: effekt::println of type Bool => Unit - Expected String but got Int. - Expected Bool but got String. - -Possible overload: list::println of type List[Bool] => Unit - Expected String but got Int. - Expected List[Bool] but got String. - -Possible overload: array::println of type Array[Int] => Unit - Expected String but got Int. - Expected Array[Int] but got String. - -Possible overload: effekt::println of type String => Unit - Expected String but got Int. - -Possible overload: list::println of type List[Int] => Unit - Expected String but got Int. - Expected List[Int] but got String. - -Possible overload: effekt::println of type Byte => Unit - Expected String but got Int. - Expected Byte but got String. - -Possible overload: list::println of type List[String] => Unit - Expected String but got Int. - Expected List[String] but got String. - -Possible overload: list::println of type List[Double] => Unit - Expected String but got Int. - Expected List[Double] but got String. - -Possible overload: array::println of type Array[String] => Unit - Expected String but got Int. - Expected Array[String] but got String. - -Possible overload: effekt::println of type Ordering => Unit - Expected String but got Int. - Expected Ordering but got String. - -Possible overload: array::println of type Array[Bool] => Unit - Expected String but got Int. - Expected Array[Bool] but got String. - -Possible overload: effekt::println of type Unit => Unit - Expected String but got Int. - Expected Unit but got String. - -Possible overload: option::println of type Option[Int] => Unit - Expected String but got Int. - Expected Option[Int] but got String. - -Possible overload: option::println of type Option[Double] => Unit - Expected String but got Int. - Expected Option[Double] but got String. - -Possible overload: array::println of type Array[Double] => Unit - Expected String but got Int. - Expected Array[Double] but got String. - -Possible overload: effekt::println of type Double => Unit - Expected String but got Int. - Expected Double but got String. - -Possible overload: effekt::println of type Int => Unit - Expected String but got Int. - Expected Int but got String. - -Possible overload: option::println of type Option[Bool] => Unit - Expected String but got Int. - Expected Option[Bool] but got String. - - println("GET ${domain}/users/${user}/resource/${resourceId}") ``` </details> Apart from that, the changes are really lightweight because of the early desugaring in the parser. I am looking forward to your feedback and ideas. --------- Co-authored-by: Jiří Beneš <[email protected]> Co-authored-by: Marvin <[email protected]> Co-authored-by: Philipp Schuster <[email protected]> Co-authored-by: Jonathan Brachthäuser <[email protected]>
1 parent f92d229 commit 6b2b191

File tree

10 files changed

+180
-17
lines changed

10 files changed

+180
-17
lines changed

effekt/jvm/src/main/scala/effekt/Runner.scala

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,15 @@ trait Runner[Executable] {
3535
def includes(stdlibPath: File): List[File] = Nil
3636

3737
/**
38-
* Modules this backend loads by default
38+
* Modules this backend loads by default.
39+
*
40+
* Invariants:
41+
* - All imports of prelude modules have to be in the prelude as well.
42+
* - The order matters and should correspond to the topological ordering with respect to the imports, that is,
43+
* if module A depends on module B, then B should come before A.
44+
* - Furthermore, each module mentioned here must import the `effekt` module as its first import.
3945
*/
40-
def prelude: List[String] = List("effekt")
46+
def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "array", "string", "ref")
4147

4248
/**
4349
* Creates a OS-specific script file that will execute the command when executed,
@@ -151,8 +157,6 @@ object JSNodeRunner extends Runner[String] {
151157

152158
override def includes(path: File): List[File] = List(path / ".." / "js")
153159

154-
override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "array", "string", "ref")
155-
156160
def checkSetup(): Either[String, Unit] =
157161
if canRunExecutable("node", "--version") then Right(())
158162
else Left("Cannot find nodejs. This is required to use the JavaScript backend.")
@@ -184,16 +188,13 @@ object JSNodeRunner extends Runner[String] {
184188
}
185189
}
186190
object JSWebRunner extends Runner[String] {
187-
import scala.sys.process.Process
188191

189192
val extension = "js"
190193

191194
def standardLibraryPath(root: File): File = root / "libraries" / "common"
192195

193196
override def includes(path: File): List[File] = List(path / ".." / "js")
194197

195-
override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "array", "string", "ref")
196-
197198
def checkSetup(): Either[String, Unit] =
198199
Left("Running js-web code directly is not supported. Use `--compile` to generate a js file / `--build` to generate a html file.")
199200

@@ -202,7 +203,6 @@ object JSWebRunner extends Runner[String] {
202203
* and then errors out, printing it's path.
203204
*/
204205
def build(path: String)(using C: Context): String =
205-
import java.nio.file.Path
206206
val out = C.config.outputPath().getAbsolutePath
207207
val jsFilePath = (out / path).unixPath
208208
val jsFileName = path.unixPath.split("/").last
@@ -231,9 +231,7 @@ object JSWebRunner extends Runner[String] {
231231
trait ChezRunner extends Runner[String] {
232232
val extension = "ss"
233233

234-
def standardLibraryPath(root: File): File = root / "libraries" / "common"
235-
236-
override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "array", "string", "ref")
234+
def standardLibraryPath(root: File): File = root / "libraries" / "common"
237235

238236
def checkSetup(): Either[String, Unit] =
239237
if canRunExecutable("scheme", "--help") then Right(())
@@ -264,17 +262,13 @@ object ChezCallCCRunner extends ChezRunner {
264262
}
265263

266264
object LLVMRunner extends Runner[String] {
267-
import scala.sys.process.Process
268265

269266
val extension = "ll"
270267

271268
def standardLibraryPath(root: File): File = root / "libraries" / "common"
272269

273270
override def includes(path: File): List[File] = List(path / ".." / "llvm")
274271

275-
override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "string") // "array", "ref")
276-
277-
278272
lazy val gccCmd = discoverExecutable(List("cc", "clang", "gcc"), List("--version"))
279273
lazy val llcCmd = discoverExecutable(List("llc", "llc-15", "llc-16"), List("--version"))
280274
lazy val optCmd = discoverExecutable(List("opt", "opt-15", "opt-16"), List("--version"))

effekt/shared/src/main/scala/effekt/RecursiveDescent.scala

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -974,8 +974,13 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
974974
case `fun` => funExpr()
975975
case `new` => newExpr()
976976
case `do` => doExpr()
977+
case _ if isString => templateString()
977978
case _ if isLiteral => literal()
978-
case _ if isVariable => variable()
979+
case _ if isVariable =>
980+
peek(1).kind match {
981+
case _: Str => templateString()
982+
case _ => variable()
983+
}
979984
case _ if isHole => hole()
980985
case _ if isTupleOrGroup => tupleOrGroup()
981986
case _ if isListLiteral => listLiteral()
@@ -1016,6 +1021,33 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
10161021
case `false` => true
10171022
case _ => isUnitLiteral
10181023
}
1024+
1025+
def isString: Boolean = peek.kind match {
1026+
case _: Str => true
1027+
case _ => false
1028+
}
1029+
1030+
def templateString(): Term =
1031+
nonterminal:
1032+
backtrack(idRef()) ~ template() match {
1033+
// We do not need to apply any transformation if there are no splices
1034+
case _ ~ Template(str :: Nil, Nil) => StringLit(str)
1035+
case _ ~ Template(strs, Nil) => fail("Cannot occur")
1036+
// s"a${x}b${y}" ~> s { do literal("a"); do splice(x); do literal("b"); do splice(y) }
1037+
case id ~ Template(strs, args) =>
1038+
val target = id.getOrElse(IdRef(Nil, "s"))
1039+
val doLits = strs.map { s =>
1040+
Do(None, IdRef(Nil, "literal"), Nil, List(StringLit(s)), Nil)
1041+
}
1042+
val doSplices = args.map { arg =>
1043+
Do(None, IdRef(Nil, "splice"), Nil, List(arg), Nil)
1044+
}
1045+
val body = interleave(doLits, doSplices)
1046+
.foldRight(Return(UnitLit())) { (term, acc) => ExprStmt(term, acc) }
1047+
val blk = BlockLiteral(Nil, Nil, Nil, body)
1048+
Call(IdTarget(target), Nil, Nil, List(blk))
1049+
}
1050+
10191051
def literal(): Literal =
10201052
nonterminal:
10211053
peek.kind match {
@@ -1272,6 +1304,12 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
12721304
case Fail(_, _) => position = before; None
12731305
}
12741306

1307+
def interleave[A](xs: List[A], ys: List[A]): List[A] = (xs, ys) match {
1308+
case (x :: xs, y :: ys) => x :: y :: interleave(xs, ys)
1309+
case (Nil, ys) => ys
1310+
case (xs, Nil) => xs
1311+
}
1312+
12751313
/**
12761314
* Tiny combinator DSL to sequence parsers
12771315
*/

examples/benchmarks/other/unify.effekt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ module examples/benchmarks/unify
44
import examples/benchmarks/runner
55
import map
66
import result
7-
import bytearray
87
import stream
8+
import bytearray
99

1010
type Type {
1111
Var(name: String)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
GET https://api.effekt-lang.org/users/effekt/resource/42
2+
Fix point combinator: \ f -> (\ x -> f x x) \ x -> f x x
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import stringbuffer
2+
3+
type Expr {
4+
Var(id: String)
5+
Abs(param: String, body: Expr)
6+
App(fn: Expr, arg: Expr)
7+
}
8+
9+
def pretty { prog: () => Unit / {literal, splice[Expr]} }: String = {
10+
with stringBuffer
11+
try {
12+
prog()
13+
do flush()
14+
} with literal { s =>
15+
resume(do write(s))
16+
} with splice[Expr] { expr =>
17+
expr match {
18+
case Var(id) =>
19+
do write(id)
20+
case App(Abs(param, body), arg) =>
21+
do write(pretty"(${Abs(param, body)}) ${arg}")
22+
case App(fn, arg) =>
23+
do write(pretty"${fn} ${arg}")
24+
case Abs(param, body) =>
25+
do write(s"\\ ${param} -> " ++ pretty"${body}")
26+
}
27+
resume(())
28+
}
29+
}
30+
31+
def main() = {
32+
val domain = "https://api.effekt-lang.org"
33+
val user = "effekt"
34+
val resourceId = 42
35+
println("GET ${domain}/users/${user}/resource/${resourceId.show}")
36+
37+
val fixpoint = Abs("f", App(Abs("x", App(Var("f"), App(Var("x"), Var("x")))), Abs("x", App(Var("f"), App(Var("x"), Var("x"))))))
38+
println(pretty"Fix point combinator: ${fixpoint}")
39+
}

examples/stdlib/acme.effekt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import seq
3333
import set
3434
import stream
3535
import string
36+
import stringbuffer
3637
import test
3738
import tty
3839
import union_find

examples/stdlib/map/counter.effekt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import map
2+
23
import bytearray
34

45
def counter(words: List[String]): Map[String, Int] = {

examples/stdlib/set/unique.effekt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import set
2+
23
import bytearray
34

45
def unique(words: List[String]): Set[String] = {

libraries/common/effekt.effekt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,28 @@ def repeat(n: Int) { action: () => Unit } = each(0, n) { n => action() }
736736

737737
def repeat(n: Int) { action: () {Control} => Unit } = each(0, n) { (n) {l} => action() {l} }
738738

739+
740+
// Splices
741+
// =======
742+
//
743+
// The Effekt compiler translates the string interpolation
744+
//
745+
// "foo ${42} bar ${43}"
746+
//
747+
// to the following stream of `literal`s and `splice[Int]`s:
748+
//
749+
// do literal("foo "); do splice(42); do literal(" bar "); do splice(43)
750+
//
751+
// The stream is wrapped into a handler function, which defaults to `stringbuffer::s`
752+
//
753+
// s { ... }
754+
//
755+
// but can be specified by prefixing the string interpolation `custom"..."`.
756+
757+
effect literal(s: String): Unit
758+
effect splice[A](x: A): Unit
759+
760+
739761
namespace internal {
740762
namespace boxing {
741763
// For boxing polymorphic values
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
module stringbuffer
2+
3+
import bytearray
4+
5+
interface StringBuffer {
6+
def write(str: String): Unit
7+
def flush(): String
8+
}
9+
10+
def stringBuffer[A] { prog: => A / StringBuffer }: A = {
11+
val initialCapacity = 128
12+
var buffer = bytearray::allocate(initialCapacity)
13+
// next free index to write to
14+
var pos = 0
15+
16+
def ensureCapacity(sizeToAdd: Int): Unit = {
17+
val cap = buffer.size - pos + 1
18+
if (sizeToAdd <= cap) ()
19+
else {
20+
// Double the capacity while ensuring the required capacity
21+
val newSize = max(buffer.size * 2, buffer.size + sizeToAdd)
22+
buffer = buffer.resize(newSize)
23+
}
24+
}
25+
26+
try { prog() }
27+
with StringBuffer {
28+
def write(str) = {
29+
val bytes = fromString(str)
30+
ensureCapacity(bytes.size)
31+
bytes.foreach { b =>
32+
buffer.unsafeSet(pos, b)
33+
pos = pos + 1
34+
}
35+
resume(())
36+
}
37+
def flush() = {
38+
// resize buffer to strip trailing zeros that otherwise would be converted into 0x00 characters
39+
val str = bytearray::resize(buffer, pos).toString()
40+
// after flushing, the stringbuffer should be empty again
41+
buffer = bytearray::allocate(initialCapacity)
42+
resume(str)
43+
}
44+
}
45+
}
46+
47+
/// Handler for string interpolation using a string buffer
48+
def s { prog: () => Unit / { literal, splice[String] } }: String =
49+
stringBuffer {
50+
try { prog(); do flush() }
51+
with splice[String] { x => resume(do write(x)) }
52+
with literal { s => resume(do write(s)) }
53+
}
54+
55+
namespace examples {
56+
def main() = {
57+
with stringBuffer
58+
do write("hello")
59+
do write(", world")
60+
// prints `hello, world`
61+
println(do flush())
62+
// prints the empty string
63+
println(do flush())
64+
}
65+
}

0 commit comments

Comments
 (0)