Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic string interpolation / string templates #743

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
40 changes: 39 additions & 1 deletion effekt/shared/src/main/scala/effekt/RecursiveDescent.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
*/
Expand Down
2 changes: 2 additions & 0 deletions examples/pos/string_interpolation.check
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions examples/pos/string_interpolation.effekt
Original file line number Diff line number Diff line change
@@ -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}")
}

18 changes: 18 additions & 0 deletions libraries/common/splice.effekt
Original file line number Diff line number Diff line change
@@ -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))
}
}
58 changes: 58 additions & 0 deletions libraries/common/stringbuffer.effekt
Original file line number Diff line number Diff line change
@@ -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())
}
}
Loading