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

Merged
merged 24 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d4661c3
first draft
dvdvgt Dec 12, 2024
8135ab6
add tests
dvdvgt Dec 12, 2024
3526cea
add rudimentary desugaring in parser
dvdvgt Jan 9, 2025
9ba3039
add splice module to prelude
dvdvgt Jan 9, 2025
2353267
splice handler and stringbuffer
dvdvgt Jan 9, 2025
497b538
allow `s"..."` syntax
dvdvgt Jan 10, 2025
77d0e7f
ignore new test in chez backend
dvdvgt Jan 13, 2025
b52629a
resolve error in WebTests
dvdvgt Jan 13, 2025
1017d8a
rework buffer's growth heuristic
dvdvgt Jan 13, 2025
fe5eddf
rewrite to impl stringbuffer as a handler
dvdvgt Jan 15, 2025
e4f24a6
add second test case using custom interpolation impl
dvdvgt Jan 15, 2025
d3cef35
Fix unknown compiler crash printing 'null' (#780)
jiribenes Jan 15, 2025
cc4ca22
fix flushing so that no 0x00 chars appear
dvdvgt Jan 17, 2025
2e36f3d
fix unneeded allocation
dvdvgt Jan 17, 2025
0aa6431
Fix byte-related LLVM segfaults (#785)
marvinborner Jan 21, 2025
e7c16c7
Merge branch 'master' into feature/string-interpolation
dvdvgt Jan 21, 2025
111fcd8
add bytearray, stringbuffer and splice to prelude
dvdvgt Jan 23, 2025
1aa898d
Merge branch 'master' into feature/string-interpolation
dvdvgt Jan 24, 2025
6c69e17
cherry-pick: Add ByteArray support on Chez (#791)
jiribenes Jan 22, 2025
fa54c8e
remove unneeded `import bytearray`
dvdvgt Jan 27, 2025
ea19f7a
add bytearray to chez prelude
dvdvgt Jan 27, 2025
f10a2ad
Unify preludes across backends
b-studios Jan 28, 2025
a5d2498
Drop splice as separate stdlib file and reduce prelude a bit
b-studios Jan 28, 2025
c46b99c
Fix imports in tests
b-studios Jan 28, 2025
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 @@ -33,6 +33,9 @@ abstract class ChezSchemeTests extends EffektTests {
examplesDir / "pos" / "unsafe_cont.effekt",
examplesDir / "pos" / "propagators.effekt",

// bytearray is not implemented
examplesDir / "pos" / "string_interpolation.effekt",

dvdvgt marked this conversation as resolved.
Show resolved Hide resolved
// the number representations differ in JS and Chez
examplesDir / "casestudies" / "ad.effekt.md",
examplesDir / "casestudies" / "inference.effekt.md",
Expand Down
15 changes: 8 additions & 7 deletions effekt/shared/src/main/scala/effekt/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ trait Compiler[Executable] {
* - Server / Driver to typecheck and report type errors in VSCode
*/
def runFrontend(source: Source)(using C: Context): Option[Module] =
def getStackTrace(e: Throwable): String =
val stringWriter = new java.io.StringWriter()
e.printStackTrace(new java.io.PrintWriter(stringWriter))
stringWriter.toString

try {
val res = Frontend(source).map { res =>
val mod = res.mod
Expand All @@ -128,15 +133,11 @@ trait Compiler[Executable] {
None
case e @ CompilerPanic(msg) =>
C.report(msg)
e.getStackTrace.foreach { line =>
C.info(" at " + line)
}
C.info(getStackTrace(e))
None
case e =>
C.info("Effekt Compiler Crash: " + e.getMessage)
e.getStackTrace.foreach { line =>
C.info(" at " + line)
}
C.info("Effekt Compiler Crash: " + e.toString)
C.info(getStackTrace(e))
None
}

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))
}
}
59 changes: 59 additions & 0 deletions libraries/common/stringbuffer.effekt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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)
val newBuffer = bytearray::allocate(newSize)
buffer = buffer.resize(newSize)
dvdvgt marked this conversation as resolved.
Show resolved Hide resolved
}
}

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