diff --git a/effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala b/effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala index bf89f694c..1673689d5 100644 --- a/effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala +++ b/effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala @@ -37,6 +37,9 @@ abstract class ChezSchemeTests extends EffektTests { examplesDir / "pos" / "unsafe_cont.effekt", examplesDir / "pos" / "propagators.effekt", + // bytearray is not implemented + examplesDir / "pos" / "string_interpolation.effekt", + // the number representations differ in JS and Chez examplesDir / "casestudies" / "ad.effekt.md", examplesDir / "casestudies" / "inference.effekt.md", diff --git a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala index 65242968f..9db4032b3 100644 --- a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala +++ b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala @@ -974,8 +974,13 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) case `fun` => funExpr() case `new` => newExpr() case `do` => doExpr() + case _ if isString => templateString() case _ if isLiteral => literal() - case _ if isVariable => variable() + case _ if isVariable => + peek(1).kind match { + case _: Str => templateString() + case _ => variable() + } case _ if isHole => hole() case _ if isTupleOrGroup => tupleOrGroup() case _ if isListLiteral => listLiteral() @@ -1016,6 +1021,33 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) case `false` => true case _ => isUnitLiteral } + + def isString: Boolean = peek.kind match { + case _: Str => true + case _ => false + } + + def templateString(): Term = + nonterminal: + backtrack(idRef()) ~ template() match { + // We do not need to apply any transformation if there are no splices + case _ ~ Template(str :: Nil, Nil) => StringLit(str) + case _ ~ Template(strs, Nil) => fail("Cannot occur") + // s"a${x}b${y}" ~> s { do literal("a"); do splice(x); do literal("b"); do splice(y) } + case id ~ Template(strs, args) => + val target = id.getOrElse(IdRef(Nil, "s")) + val doLits = strs.map { s => + Do(None, IdRef(Nil, "literal"), Nil, List(StringLit(s)), Nil) + } + val doSplices = args.map { arg => + Do(None, IdRef(Nil, "splice"), Nil, List(arg), Nil) + } + val body = interleave(doLits, doSplices) + .foldRight(Return(UnitLit())) { (term, acc) => ExprStmt(term, acc) } + val blk = BlockLiteral(Nil, Nil, Nil, body) + Call(IdTarget(target), Nil, Nil, List(blk)) + } + def literal(): Literal = nonterminal: peek.kind match { @@ -1272,6 +1304,12 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) case Fail(_, _) => position = before; None } + def interleave[A](xs: List[A], ys: List[A]): List[A] = (xs, ys) match { + case (x :: xs, y :: ys) => x :: y :: interleave(xs, ys) + case (Nil, ys) => ys + case (xs, Nil) => xs + } + /** * Tiny combinator DSL to sequence parsers */ diff --git a/examples/pos/string_interpolation.check b/examples/pos/string_interpolation.check new file mode 100644 index 000000000..04bcd4f9e --- /dev/null +++ b/examples/pos/string_interpolation.check @@ -0,0 +1,2 @@ +GET https://api.effekt-lang.org/users/effekt/resource/42 +Fix point combinator: \ f -> (\ x -> f x x) \ x -> f x x \ No newline at end of file diff --git a/examples/pos/string_interpolation.effekt b/examples/pos/string_interpolation.effekt new file mode 100644 index 000000000..60aeb0b07 --- /dev/null +++ b/examples/pos/string_interpolation.effekt @@ -0,0 +1,41 @@ +import splice +import stringbuffer + +type Expr { + Var(id: String) + Abs(param: String, body: Expr) + App(fn: Expr, arg: Expr) +} + +def pretty { prog: () => Unit / {literal, splice[Expr]} }: String = { + with stringBuffer + try { + prog() + do flush() + } with literal { s => + resume(do write(s)) + } with splice[Expr] { expr => + expr match { + case Var(id) => + do write(id) + case App(Abs(param, body), arg) => + do write(pretty"(${Abs(param, body)}) ${arg}") + case App(fn, arg) => + do write(pretty"${fn} ${arg}") + case Abs(param, body) => + do write(s"\\ ${param} -> " ++ pretty"${body}") + } + resume(()) + } +} + +def main() = { + val domain = "https://api.effekt-lang.org" + val user = "effekt" + val resourceId = 42 + println("GET ${domain}/users/${user}/resource/${resourceId.show}") + + 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")))))) + println(pretty"Fix point combinator: ${fixpoint}") +} + diff --git a/libraries/common/splice.effekt b/libraries/common/splice.effekt new file mode 100644 index 000000000..9ba74d159 --- /dev/null +++ b/libraries/common/splice.effekt @@ -0,0 +1,18 @@ +module splice + +import stringbuffer + +effect literal(s: String): Unit +effect splice[A](x: A): Unit + +def s { prog: () => Unit / { literal, splice[String] } }: String = { + with stringBuffer + try { + prog() + do flush() + } with splice[String] { x => + resume(do write(x)) + } with literal { s => + resume(do write(s)) + } +} diff --git a/libraries/common/stringbuffer.effekt b/libraries/common/stringbuffer.effekt new file mode 100644 index 000000000..3b6d3acb7 --- /dev/null +++ b/libraries/common/stringbuffer.effekt @@ -0,0 +1,58 @@ +module stringbuffer + +import bytearray + +interface StringBuffer { + def write(str: String): Unit + def flush(): String +} + +def stringBuffer[A] { prog: => A / StringBuffer }: A = { + val initialCapacity = 128 + var buffer = bytearray::allocate(initialCapacity) + // next free index to write to + var pos = 0 + + def ensureCapacity(sizeToAdd: Int): Unit = { + val cap = buffer.size - pos + 1 + if (sizeToAdd <= cap) () + else { + // Double the capacity while ensuring the required capacity + val newSize = max(buffer.size * 2, buffer.size + sizeToAdd) + buffer = buffer.resize(newSize) + } + } + + try { prog() } + with StringBuffer { + def write(str) = { + val bytes = fromString(str) + ensureCapacity(bytes.size) + bytes.foreach { b => + buffer.unsafeSet(pos, b) + pos = pos + 1 + } + resume(()) + } + def flush() = { + // resize buffer to strip trailing zeros that otherwise would be converted into 0x00 characters + buffer = bytearray::resize(buffer, pos) + val str = buffer.toString() + // after flushing, the stringbuffer should be empty again + buffer = bytearray::allocate(initialCapacity) + resume(str) + } + } +} + +namespace examples { + def main() = { + with stringBuffer + do write("hello") + do write(", world") + // prints `hello, world` + println(do flush()) + // prints the empty string + println(do flush()) + } +}