From d4661c31bd5544652e9e9308fec78c5ff799f20b Mon Sep 17 00:00:00 2001 From: dvdvgt Date: Thu, 12 Dec 2024 12:37:29 +0100 Subject: [PATCH 01/15] first draft --- .../main/scala/effekt/RecursiveDescent.scala | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala index 8dcffbeb7..a60c2a5a0 100644 --- a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala +++ b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala @@ -974,6 +974,7 @@ 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 isHole => hole() @@ -1016,6 +1017,24 @@ 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 = + template() match { + case Template(s :: strs, arg :: args) => + strs.zip(args) + .foldLeft(binaryOp(StringLit(s), `++`, arg)) { + case (acc, ("", arg)) => binaryOp(acc, `++`, arg) + case (acc, (s, arg)) => binaryOp(acc, `++`, binaryOp(StringLit(s), `++`, arg)) + } + case Template(List(s), Nil) => StringLit(s) + case _ => fail("cannot occur") + } + def literal(): Literal = nonterminal: peek.kind match { From 8135ab654353bd369ee4aaff26acdd0b6ccc1ec1 Mon Sep 17 00:00:00 2001 From: dvdvgt Date: Thu, 12 Dec 2024 12:37:39 +0100 Subject: [PATCH 02/15] add tests --- examples/pos/string_templates.check | 1 + examples/pos/string_templates.effekt | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 examples/pos/string_templates.check create mode 100644 examples/pos/string_templates.effekt diff --git a/examples/pos/string_templates.check b/examples/pos/string_templates.check new file mode 100644 index 000000000..5aa5bee03 --- /dev/null +++ b/examples/pos/string_templates.check @@ -0,0 +1 @@ +GET https://api.effekt-lang.org/users/effekt/resource/42 diff --git a/examples/pos/string_templates.effekt b/examples/pos/string_templates.effekt new file mode 100644 index 000000000..052f3733c --- /dev/null +++ b/examples/pos/string_templates.effekt @@ -0,0 +1,8 @@ +def main() = { + val domain = "https://api.effekt-lang.org" + val user = "effekt" + val resourceId = 42 + + println("GET ${domain}/users/${user}/resource/${resourceId.show}") +} + From 3526cea095ae30024ff2e7e2453cfd38d8ff3da5 Mon Sep 17 00:00:00 2001 From: dvdvgt Date: Thu, 9 Jan 2025 22:30:52 +0100 Subject: [PATCH 03/15] add rudimentary desugaring in parser --- .../main/scala/effekt/RecursiveDescent.scala | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala index a60c2a5a0..7995e5e89 100644 --- a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala +++ b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala @@ -545,7 +545,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) val ff = maybeFeatureFlag() next().kind match { case Str(contents, _) => ExternInclude(ff, "", Some(contents)) - case _ => fail("Expected string literal.") + case t => fail(s"Expected string literal but got ${t}.") } def externFun(): Def = @@ -602,7 +602,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) nonterminal: next().kind match { case Str(s, _) => s - case _ => fail("Expected string literal.") + case t => fail(s"Expected string literal but got ${t}.") } @@ -1024,15 +1024,23 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) } def templateString(): Term = - template() match { - case Template(s :: strs, arg :: args) => - strs.zip(args) - .foldLeft(binaryOp(StringLit(s), `++`, arg)) { - case (acc, ("", arg)) => binaryOp(acc, `++`, arg) - case (acc, (s, arg)) => binaryOp(acc, `++`, binaryOp(StringLit(s), `++`, arg)) - } - case Template(List(s), Nil) => StringLit(s) - case _ => fail("cannot occur") + (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 (Template(strs, args)) => + val target = 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 = @@ -1291,6 +1299,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 */ From 9ba3039ea2016d4215e7e03806e6bf01393928ab Mon Sep 17 00:00:00 2001 From: dvdvgt Date: Thu, 9 Jan 2025 22:34:20 +0100 Subject: [PATCH 04/15] add splice module to prelude --- effekt/js/src/main/scala/effekt/EffektConfig.scala | 1 + effekt/jvm/src/main/scala/effekt/Runner.scala | 6 +++--- effekt/shared/src/main/scala/effekt/RecursiveDescent.scala | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/effekt/js/src/main/scala/effekt/EffektConfig.scala b/effekt/js/src/main/scala/effekt/EffektConfig.scala index bc24d9a28..7b1fa501a 100644 --- a/effekt/js/src/main/scala/effekt/EffektConfig.scala +++ b/effekt/js/src/main/scala/effekt/EffektConfig.scala @@ -25,6 +25,7 @@ trait EffektConfig { "exception", "array", "string", + "splice", "ref" ) diff --git a/effekt/jvm/src/main/scala/effekt/Runner.scala b/effekt/jvm/src/main/scala/effekt/Runner.scala index ed582464c..7d3f61d39 100644 --- a/effekt/jvm/src/main/scala/effekt/Runner.scala +++ b/effekt/jvm/src/main/scala/effekt/Runner.scala @@ -151,7 +151,7 @@ object JSNodeRunner extends Runner[String] { override def includes(path: File): List[File] = List(path / ".." / "js") - override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "array", "string", "ref") + override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "array", "string", "splice", "ref") def checkSetup(): Either[String, Unit] = if canRunExecutable("node", "--version") then Right(()) @@ -192,7 +192,7 @@ object JSWebRunner extends Runner[String] { override def includes(path: File): List[File] = List(path / ".." / "js") - override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "array", "string", "ref") + override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "array", "string", "splice", "ref") def checkSetup(): Either[String, Unit] = Left("Running js-web code directly is not supported. Use `--compile` to generate a js file / `--build` to generate a html file.") @@ -267,7 +267,7 @@ object LLVMRunner extends Runner[String] { override def includes(path: File): List[File] = List(path / ".." / "llvm") - override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "string") // "array", "ref") + override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "string", "splice") // "array", "ref") lazy val gccCmd = discoverExecutable(List("cc", "clang", "gcc"), List("--version")) diff --git a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala index 7995e5e89..8fc11373e 100644 --- a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala +++ b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala @@ -545,7 +545,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) val ff = maybeFeatureFlag() next().kind match { case Str(contents, _) => ExternInclude(ff, "", Some(contents)) - case t => fail(s"Expected string literal but got ${t}.") + case t => fail("Expected string literal.") } def externFun(): Def = @@ -602,7 +602,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) nonterminal: next().kind match { case Str(s, _) => s - case t => fail(s"Expected string literal but got ${t}.") + case t => fail("Expected string literal.") } From 2353267e36919d044f5228b071e56ef1bf932030 Mon Sep 17 00:00:00 2001 From: dvdvgt Date: Thu, 9 Jan 2025 22:45:52 +0100 Subject: [PATCH 05/15] splice handler and stringbuffer --- libraries/common/splice.effekt | 18 +++++++++ libraries/common/stringbuffer.effekt | 60 ++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 libraries/common/splice.effekt create mode 100644 libraries/common/stringbuffer.effekt diff --git a/libraries/common/splice.effekt b/libraries/common/splice.effekt new file mode 100644 index 000000000..602a59ea1 --- /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 = { + var buffer = StringBuffer() + try { + prog() + buffer.flush() + } with splice[String] { x => + resume(buffer.write(x)) + } with literal { s => + resume(buffer.write(s)) + } +} diff --git a/libraries/common/stringbuffer.effekt b/libraries/common/stringbuffer.effekt new file mode 100644 index 000000000..41bfe3599 --- /dev/null +++ b/libraries/common/stringbuffer.effekt @@ -0,0 +1,60 @@ +module stringbuffer + +import bytearray + +interface StringBuffer { + def write(str: String): Unit + def flush(): String +} + +def StringBuffer(): StringBuffer at {global} = { + val initialCapacity = 128 + var buffer in global = bytearray::allocate(initialCapacity) + var pos in global = 0 + + def ensureCapacity(requiredSize: Int): Unit = { + val cap = buffer.size + if (requiredSize <= cap) () + else { + val newSize = min(cap * 2, requiredSize) + val newBuffer = bytearray::allocate(newSize) + pos = 0 + buffer.foreach { b => + newBuffer.unsafeSet(pos, b) + pos = pos + 1 + } + buffer = newBuffer + } + } + + def stringBuffer = new StringBuffer { + def write(str) = { + val bytes = fromString(str) + ensureCapacity(bytes.size) + bytes.foreach { b => + buffer.unsafeSet(pos, b) + pos = pos + 1 + } + } + + def flush() = { + val str = buffer.toString() + buffer = bytearray::allocate(initialCapacity) + str + } + } + + stringBuffer +} + +namespace examples { + def main() = { + def buffer = StringBuffer() + buffer.write("hello") + buffer.write(", world") + // prints `hello, world` + println(buffer.flush()) + // prints the empty string + println(buffer.flush()) + } +} From 497b53829dc2ce3ce733b566d13caffd6af3417c Mon Sep 17 00:00:00 2001 From: dvdvgt Date: Fri, 10 Jan 2025 16:43:25 +0100 Subject: [PATCH 06/15] allow `s"..."` syntax --- effekt/jvm/src/main/scala/effekt/Runner.scala | 6 +-- .../main/scala/effekt/RecursiveDescent.scala | 43 +++++++++++-------- examples/pos/string_templates.effekt | 2 + 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/effekt/jvm/src/main/scala/effekt/Runner.scala b/effekt/jvm/src/main/scala/effekt/Runner.scala index 7d3f61d39..ed582464c 100644 --- a/effekt/jvm/src/main/scala/effekt/Runner.scala +++ b/effekt/jvm/src/main/scala/effekt/Runner.scala @@ -151,7 +151,7 @@ object JSNodeRunner extends Runner[String] { override def includes(path: File): List[File] = List(path / ".." / "js") - override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "array", "string", "splice", "ref") + override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "array", "string", "ref") def checkSetup(): Either[String, Unit] = if canRunExecutable("node", "--version") then Right(()) @@ -192,7 +192,7 @@ object JSWebRunner extends Runner[String] { override def includes(path: File): List[File] = List(path / ".." / "js") - override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "array", "string", "splice", "ref") + override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "array", "string", "ref") def checkSetup(): Either[String, Unit] = Left("Running js-web code directly is not supported. Use `--compile` to generate a js file / `--build` to generate a html file.") @@ -267,7 +267,7 @@ object LLVMRunner extends Runner[String] { override def includes(path: File): List[File] = List(path / ".." / "llvm") - override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "string", "splice") // "array", "ref") + override def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "string") // "array", "ref") lazy val gccCmd = discoverExecutable(List("cc", "clang", "gcc"), List("--version")) diff --git a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala index 8fc11373e..ddd68a454 100644 --- a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala +++ b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala @@ -976,7 +976,11 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) 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() @@ -1024,24 +1028,25 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) } def templateString(): Term = - (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 (Template(strs, args)) => - val target = 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)) - } + 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: diff --git a/examples/pos/string_templates.effekt b/examples/pos/string_templates.effekt index 052f3733c..8e184d2d9 100644 --- a/examples/pos/string_templates.effekt +++ b/examples/pos/string_templates.effekt @@ -1,3 +1,5 @@ +import splice + def main() = { val domain = "https://api.effekt-lang.org" val user = "effekt" From 77d0e7f9a7358eb66104969e105c88f37d1fefb0 Mon Sep 17 00:00:00 2001 From: dvdvgt Date: Mon, 13 Jan 2025 13:05:47 +0100 Subject: [PATCH 07/15] ignore new test in chez backend --- effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala b/effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala index 00f2f659f..3f7e295a2 100644 --- a/effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala +++ b/effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala @@ -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_templates.effekt", + // the number representations differ in JS and Chez examplesDir / "casestudies" / "ad.effekt.md", examplesDir / "casestudies" / "inference.effekt.md", From b52629ab27ccd98c5d6588056b0706dc1c20742f Mon Sep 17 00:00:00 2001 From: dvdvgt Date: Mon, 13 Jan 2025 13:06:36 +0100 Subject: [PATCH 08/15] resolve error in WebTests --- effekt/js/src/main/scala/effekt/EffektConfig.scala | 1 - effekt/shared/src/main/scala/effekt/RecursiveDescent.scala | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/effekt/js/src/main/scala/effekt/EffektConfig.scala b/effekt/js/src/main/scala/effekt/EffektConfig.scala index 7b1fa501a..bc24d9a28 100644 --- a/effekt/js/src/main/scala/effekt/EffektConfig.scala +++ b/effekt/js/src/main/scala/effekt/EffektConfig.scala @@ -25,7 +25,6 @@ trait EffektConfig { "exception", "array", "string", - "splice", "ref" ) diff --git a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala index ddd68a454..63970dcd0 100644 --- a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala +++ b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala @@ -545,7 +545,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) val ff = maybeFeatureFlag() next().kind match { case Str(contents, _) => ExternInclude(ff, "", Some(contents)) - case t => fail("Expected string literal.") + case _ => fail("Expected string literal.") } def externFun(): Def = @@ -602,7 +602,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) nonterminal: next().kind match { case Str(s, _) => s - case t => fail("Expected string literal.") + case _ => fail("Expected string literal.") } From 1017d8ad58b71e7d0a461b81b43927c25cb33a88 Mon Sep 17 00:00:00 2001 From: dvdvgt Date: Mon, 13 Jan 2025 13:33:11 +0100 Subject: [PATCH 09/15] rework buffer's growth heuristic --- libraries/common/stringbuffer.effekt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libraries/common/stringbuffer.effekt b/libraries/common/stringbuffer.effekt index 41bfe3599..84c2b5024 100644 --- a/libraries/common/stringbuffer.effekt +++ b/libraries/common/stringbuffer.effekt @@ -10,14 +10,17 @@ interface StringBuffer { def StringBuffer(): StringBuffer at {global} = { val initialCapacity = 128 var buffer in global = bytearray::allocate(initialCapacity) + // next free index to write to var pos in global = 0 - def ensureCapacity(requiredSize: Int): Unit = { - val cap = buffer.size - if (requiredSize <= cap) () + def ensureCapacity(sizeToAdd: Int): Unit = { + val cap = buffer.size - pos + 1 + if (sizeToAdd <= cap) () else { - val newSize = min(cap * 2, requiredSize) + // Double the capacity while ensuring the required capacity + val newSize = max(buffer.size * 2, buffer.size + sizeToAdd) val newBuffer = bytearray::allocate(newSize) + // copy content of old buffer into newly allocated buffer pos = 0 buffer.foreach { b => newBuffer.unsafeSet(pos, b) From fe5eddfb591be7182325909e9c59bf40b7e6ddd9 Mon Sep 17 00:00:00 2001 From: dvdvgt Date: Wed, 15 Jan 2025 14:52:20 +0100 Subject: [PATCH 10/15] rewrite to impl stringbuffer as a handler --- libraries/common/splice.effekt | 8 +++---- libraries/common/stringbuffer.effekt | 34 +++++++++++----------------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/libraries/common/splice.effekt b/libraries/common/splice.effekt index 602a59ea1..9ba74d159 100644 --- a/libraries/common/splice.effekt +++ b/libraries/common/splice.effekt @@ -6,13 +6,13 @@ effect literal(s: String): Unit effect splice[A](x: A): Unit def s { prog: () => Unit / { literal, splice[String] } }: String = { - var buffer = StringBuffer() + with stringBuffer try { prog() - buffer.flush() + do flush() } with splice[String] { x => - resume(buffer.write(x)) + resume(do write(x)) } with literal { s => - resume(buffer.write(s)) + resume(do write(s)) } } diff --git a/libraries/common/stringbuffer.effekt b/libraries/common/stringbuffer.effekt index 84c2b5024..ea6c35a6c 100644 --- a/libraries/common/stringbuffer.effekt +++ b/libraries/common/stringbuffer.effekt @@ -7,11 +7,11 @@ interface StringBuffer { def flush(): String } -def StringBuffer(): StringBuffer at {global} = { +def stringBuffer[A] { prog: => A / StringBuffer }: A = { val initialCapacity = 128 - var buffer in global = bytearray::allocate(initialCapacity) + var buffer = bytearray::allocate(initialCapacity) // next free index to write to - var pos in global = 0 + var pos = 0 def ensureCapacity(sizeToAdd: Int): Unit = { val cap = buffer.size - pos + 1 @@ -19,18 +19,12 @@ def StringBuffer(): StringBuffer at {global} = { else { // Double the capacity while ensuring the required capacity val newSize = max(buffer.size * 2, buffer.size + sizeToAdd) - val newBuffer = bytearray::allocate(newSize) - // copy content of old buffer into newly allocated buffer - pos = 0 - buffer.foreach { b => - newBuffer.unsafeSet(pos, b) - pos = pos + 1 - } - buffer = newBuffer + buffer = bytearray::resize(buffer, newSize) } } - def stringBuffer = new StringBuffer { + try { prog() } + with StringBuffer { def write(str) = { val bytes = fromString(str) ensureCapacity(bytes.size) @@ -38,26 +32,24 @@ def StringBuffer(): StringBuffer at {global} = { buffer.unsafeSet(pos, b) pos = pos + 1 } + resume(()) } - def flush() = { val str = buffer.toString() buffer = bytearray::allocate(initialCapacity) - str + resume(str) } } - - stringBuffer } namespace examples { def main() = { - def buffer = StringBuffer() - buffer.write("hello") - buffer.write(", world") + with stringBuffer + do write("hello") + do write(", world") // prints `hello, world` - println(buffer.flush()) + println(do flush()) // prints the empty string - println(buffer.flush()) + println(do flush()) } } From e4f24a6e7386058138fe782414e6b1252b6ad330 Mon Sep 17 00:00:00 2001 From: dvdvgt Date: Wed, 15 Jan 2025 15:45:29 +0100 Subject: [PATCH 11/15] add second test case using custom interpolation impl --- .../test/scala/effekt/ChezSchemeTests.scala | 2 +- ...lates.check => string_interpolation.check} | 1 + examples/pos/string_interpolation.effekt | 41 +++++++++++++++++++ examples/pos/string_templates.effekt | 10 ----- 4 files changed, 43 insertions(+), 11 deletions(-) rename examples/pos/{string_templates.check => string_interpolation.check} (50%) create mode 100644 examples/pos/string_interpolation.effekt delete mode 100644 examples/pos/string_templates.effekt diff --git a/effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala b/effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala index 3f7e295a2..87a04898d 100644 --- a/effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala +++ b/effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala @@ -34,7 +34,7 @@ abstract class ChezSchemeTests extends EffektTests { examplesDir / "pos" / "propagators.effekt", // bytearray is not implemented - examplesDir / "pos" / "string_templates.effekt", + examplesDir / "pos" / "string_interpolation.effekt", // the number representations differ in JS and Chez examplesDir / "casestudies" / "ad.effekt.md", diff --git a/examples/pos/string_templates.check b/examples/pos/string_interpolation.check similarity index 50% rename from examples/pos/string_templates.check rename to examples/pos/string_interpolation.check index 5aa5bee03..04bcd4f9e 100644 --- a/examples/pos/string_templates.check +++ b/examples/pos/string_interpolation.check @@ -1 +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/examples/pos/string_templates.effekt b/examples/pos/string_templates.effekt deleted file mode 100644 index 8e184d2d9..000000000 --- a/examples/pos/string_templates.effekt +++ /dev/null @@ -1,10 +0,0 @@ -import splice - -def main() = { - val domain = "https://api.effekt-lang.org" - val user = "effekt" - val resourceId = 42 - - println("GET ${domain}/users/${user}/resource/${resourceId.show}") -} - From d3cef35ad87896027f3c807e8718b41320fb099a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Wed, 15 Jan 2025 20:52:59 +0100 Subject: [PATCH 12/15] Fix unknown compiler crash printing 'null' (#780) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the `Effect Compiler Crash: null` part of #760. The actual problem there was that `e.getMessage` can return `null` and it does for... _StackOverflowException_ 🥳. Looking at the docs, it's recommended to use `e.toString`. While I was at it, I modified stack trace printing, now we do it properly by using `.printStackTrace`. This is very related to #731, I just want to get this out quickly to unblock other problems. --- .../shared/src/main/scala/effekt/Compiler.scala | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Compiler.scala b/effekt/shared/src/main/scala/effekt/Compiler.scala index d7062642c..d60c2780f 100644 --- a/effekt/shared/src/main/scala/effekt/Compiler.scala +++ b/effekt/shared/src/main/scala/effekt/Compiler.scala @@ -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 @@ -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 } From cc4ca22eb1e5d62fd4aa8b31bd6da1daeecb840b Mon Sep 17 00:00:00 2001 From: dvdvgt Date: Fri, 17 Jan 2025 11:50:24 +0100 Subject: [PATCH 13/15] fix flushing so that no 0x00 chars appear --- libraries/common/stringbuffer.effekt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/common/stringbuffer.effekt b/libraries/common/stringbuffer.effekt index ea6c35a6c..5cde44636 100644 --- a/libraries/common/stringbuffer.effekt +++ b/libraries/common/stringbuffer.effekt @@ -19,7 +19,8 @@ def stringBuffer[A] { prog: => A / StringBuffer }: A = { else { // Double the capacity while ensuring the required capacity val newSize = max(buffer.size * 2, buffer.size + sizeToAdd) - buffer = bytearray::resize(buffer, newSize) + val newBuffer = bytearray::allocate(newSize) + buffer = buffer.resize(newSize) } } @@ -35,7 +36,10 @@ def stringBuffer[A] { prog: => A / StringBuffer }: A = { 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) } From 2e36f3d39600a41a2ca581455569d71abcf03ffb Mon Sep 17 00:00:00 2001 From: dvdvgt Date: Fri, 17 Jan 2025 16:01:45 +0100 Subject: [PATCH 14/15] fix unneeded allocation --- libraries/common/stringbuffer.effekt | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/common/stringbuffer.effekt b/libraries/common/stringbuffer.effekt index 5cde44636..3b6d3acb7 100644 --- a/libraries/common/stringbuffer.effekt +++ b/libraries/common/stringbuffer.effekt @@ -19,7 +19,6 @@ def stringBuffer[A] { prog: => A / StringBuffer }: A = { 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) } } From 0aa643181985c17a86e03641d6a86711967124dd Mon Sep 17 00:00:00 2001 From: Marvin Date: Tue, 21 Jan 2025 16:51:53 +0100 Subject: [PATCH 15/15] Fix byte-related LLVM segfaults (#785) Closes #783. And closes #778. I believe the way we currently handle size calculations for stack pushes/pops of non-structural types has some flaws for non-pointer values. Some changes in how we use the GEP instruction will probably fix this, but I find its syntax really confusing. I have a proof-of-concept alternate fix at the end. To bypass the calculation issues, we can also just change the size of a byte to 8 byte instead. This *hotfix* fixes the segfault in the original issue as well as all of my minified versions. Some additional explanations of the issue using an example program: ```scala def foo { action: Byte => Unit }: Unit = action(42.toByte) def main() = { foo { b => // does not segfault when printlns are swapped println(42) println(b) } } ```
Pseudo-flow with relevant (for the segfault) lines marked ``` main(stack) main(stack): alloc(3 * 8 = 24) push returnAddress_1 push sharer_5 push eraser_7 blockLit = vtable[blockLit_3289_clause_13] foo(blockLit, stack) foo(blockLit, stack): alloc(3 * 8 = 24) push returnAddress_40 push sharer_5 push eraser_7 literal = 42 byte = toByte(literal) vtable = blockLit[0] closure = blockLit[1] fp = *vtable // = blockLit_3289_clause_13 fp(closure, byte, stack) blockLit_3289_clause_13(closure, byte, stack): alloc(3 * 8 + 1 = 25) // <--------------------------- push byte push returnAddress_14 push sharer_26 push eraser_30 literal = 42 printlnInt(literal, stack) printlnInt(value, stack): // _4 str = show(value) unit = println(str) dealloc(3 * 8 = 24) pop _ pop _ pop returnAddress // _14 returnAddress(unit, stack) // <--------------------------- returnAddress_14(ret, stack): dealloc(1) pop byte erase(ret) alloc(3 * 8 = 24) push returnAddress_17 push sharer_5 push eraser_7 printlnByte(byte, stack) printlnByte(byte, stack): str = show(byte) unit = println(str) dealloc(24) pop _ pop _ pop returnAddress // _17 returnAddress(unit, stack) returnAddress_17(unit, stack): dealloc(24) pop _ pop _ pop returnAddress // _40 returnAddress(unit, stack) returnAddress_40(unit, stack) dealloc(24) pop _ pop _ pop returnAddress // _1 returnAddress(unit, stack) returnAddress_1(unit, stack): dealloc(24) pop _ pop _ pop returnAddress returnAddress(unit, stack) ```
The corresponding LLVM code of the relevant sections: ```llvm define tailcc void @blockLit_3289_clause_13(%Object %closure, i8 %b_2365, %Stack %stack) { entry: ; new blockLit_3289_clause_13, 1 parameters %stackPointer_33 = call ccc %StackPointer @stackAllocate(%Stack %stack, i64 25) ; <--------------- %b_2365_pointer_34 = getelementptr {i8}, %StackPointer %stackPointer_33, i64 0, i32 0 store i8 %b_2365, ptr %b_2365_pointer_34, !noalias !2 %returnAddress_pointer_35 = getelementptr {{i8}, %FrameHeader}, %StackPointer %stackPointer_33, i64 0, i32 1, i32 0 %sharer_pointer_36 = getelementptr {{i8}, %FrameHeader}, %StackPointer %stackPointer_33, i64 0, i32 1, i32 1 %eraser_pointer_37 = getelementptr {{i8}, %FrameHeader}, %StackPointer %stackPointer_33, i64 0, i32 1, i32 2 store ptr @returnAddress_14, ptr %returnAddress_pointer_35, !noalias !2 store ptr @sharer_26, ptr %sharer_pointer_36, !noalias !2 store ptr @eraser_30, ptr %eraser_pointer_37, !noalias !2 ; literalInt longLiteral_3291, n=42 %longLiteral_3291 = add i64 42, 0 ; substitution ; substitution [value_3 !-> longLiteral_3291] ; jump println_4 musttail call tailcc void @println_4(i64 %longLiteral_3291, %Stack %stack) ret void } ... define tailcc void @println_4(i64 %value_3, %Stack %stack) { entry: ; definition println_4, environment length 1 ; foreignCall pureApp_3284 : String(), foreign show_14, 1 values %pureApp_3284 = call ccc %Pos @show_14(i64 %value_3) ; foreignCall pureApp_3283 : Positive(), foreign println_1, 1 values %pureApp_3283 = call ccc %Pos @println_1(%Pos %pureApp_3284) ; substitution ; substitution [v_r_3164_3278 !-> pureApp_3283] ; return, 1 values %stackPointer_56 = call ccc %StackPointer @stackDeallocate(%Stack %stack, i64 24) %returnAddress_pointer_57 = getelementptr %FrameHeader, %StackPointer %stackPointer_56, i64 0, i32 0 %returnAddress_55 = load %ReturnAddress, ptr %returnAddress_pointer_57, !noalias !2 musttail call tailcc void %returnAddress_55(%Pos %pureApp_3283, %Stack %stack) ; <------------------------- ; here is the segfault, since returnAddress_55 is missing one byte! ret void } ``` --- A fix I came up with that does not involve making the size of a byte large: ``` llvm define tailcc void @blockLit_3289_clause_13(%Object %closure, %Byte %b_2365, %Stack %stack) { entry: ; new blockLit_3289_clause_13, 1 parameters ; SPLIT the allocation into two segments <----------------------- %stackPointar_33 = call ccc %StackPointer @stackAllocate(%Stack %stack, i64 1) %b_2365_pointer_34 = getelementptr %Byte, %StackPointer %stackPointar_33, i64 0 store %Byte %b_2365, ptr %b_2365_pointer_34, !noalias !2 %stackPointer_33 = call ccc %StackPointer @stackAllocate(%Stack %stack, i64 24) %returnAddress_pointer_35 = getelementptr %FrameHeader, %StackPointer %stackPointer_33, i64 0, i32 0 %sharer_pointer_36 = getelementptr %FrameHeader, %StackPointer %stackPointer_33, i64 0, i32 1 %eraser_pointer_37 = getelementptr %FrameHeader, %StackPointer %stackPointer_33, i64 0, i32 2 store ptr @returnAddress_14, ptr %returnAddress_pointer_35, !noalias !2 store ptr @sharer_26, ptr %sharer_pointer_36, !noalias !2 store ptr @eraser_30, ptr %eraser_pointer_37, !noalias !2 ; literalInt longLiteral_3291, n=42 %longLiteral_3291 = add i64 42, 0 ; substitution ; substitution [value_3 !-> longLiteral_3291] ; jump println_4 musttail call tailcc void @println_4(i64 %longLiteral_3291, %Stack %stack) ret void } ... ``` --------- Co-authored-by: Philipp Schuster --- effekt/jvm/src/test/scala/effekt/StdlibTests.scala | 9 ++------- .../main/scala/effekt/generator/llvm/Transformer.scala | 8 +------- .../src/main/scala/effekt/machine/PrettyPrinter.scala | 1 - .../src/main/scala/effekt/machine/Transformer.scala | 4 ++-- effekt/shared/src/main/scala/effekt/machine/Tree.scala | 3 ++- libraries/llvm/main.c | 1 + 6 files changed, 8 insertions(+), 18 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala b/effekt/jvm/src/test/scala/effekt/StdlibTests.scala index f74cb0e21..23b28c5e3 100644 --- a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala +++ b/effekt/jvm/src/test/scala/effekt/StdlibTests.scala @@ -41,13 +41,8 @@ class StdlibLLVMTests extends StdlibTests { override def debug = sys.env.get("EFFEKT_DEBUG").nonEmpty override def ignored: List[File] = List( - // Toplevel let-bindings (for ANSI-color-codes in output) not supported - examplesDir / "stdlib" / "test" / "test.effekt", - - // Valgrind leak/failure - examplesDir / "stdlib" / "bytearray" / "bytearray.effekt", - examplesDir / "stdlib" / "stream" / "characters.effekt", - examplesDir / "stdlib" / "stream" / "fuse_newlines.effekt", + // Syscall param write(buf) points to uninitialised byte(s) + examplesDir / "stdlib" / "io" / "filesystem" / "files.effekt", examplesDir / "stdlib" / "io" / "filesystem" / "async_file_io.effekt", examplesDir / "stdlib" / "io" / "filesystem" / "files.effekt", examplesDir / "stdlib" / "io" / "filesystem" / "wordcount.effekt", diff --git a/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala b/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala index 5cb66a27b..b20b46095 100644 --- a/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala @@ -414,7 +414,6 @@ object Transformer { case machine.Type.Int() => IntegerType64() case machine.Type.Byte() => IntegerType8() case machine.Type.Double() => DoubleType() - case machine.Type.String() => positiveType case machine.Type.Reference(tpe) => referenceType } @@ -428,9 +427,8 @@ object Transformer { case machine.Type.Prompt() => 8 // TODO Make fat? case machine.Type.Stack() => 8 // TODO Make fat? case machine.Type.Int() => 8 // TODO Make fat? - case machine.Type.Byte() => 1 + case machine.Type.Byte() => 8 case machine.Type.Double() => 8 // TODO Make fat? - case machine.Type.String() => 16 case machine.Type.Reference(_) => 16 } @@ -665,7 +663,6 @@ object Transformer { case machine.Positive() => Call("_", Ccc(), VoidType(), sharePositive, List(transform(value))) case machine.Negative() => Call("_", Ccc(), VoidType(), shareNegative, List(transform(value))) case machine.Type.Stack() => Call("_", Ccc(), VoidType(), shareResumption, List(transform(value))) - case machine.Type.String() => Call("_", Ccc(), VoidType(), shareString, List(transform(value))) }.map(emit) } @@ -674,7 +671,6 @@ object Transformer { case machine.Positive() => Call("_", Ccc(), VoidType(), erasePositive, List(transform(value))) case machine.Negative() => Call("_", Ccc(), VoidType(), eraseNegative, List(transform(value))) case machine.Type.Stack() => Call("_", Ccc(), VoidType(), eraseResumption, List(transform(value))) - case machine.Type.String() => Call("_", Ccc(), VoidType(), eraseString, List(transform(value))) }.map(emit) } @@ -702,14 +698,12 @@ object Transformer { val shareNegative = ConstantGlobal("shareNegative"); val shareResumption = ConstantGlobal("shareResumption"); val shareFrames = ConstantGlobal("shareFrames"); - val shareString = ConstantGlobal("sharePositive"); val eraseObject = ConstantGlobal("eraseObject"); val erasePositive = ConstantGlobal("erasePositive"); val eraseNegative = ConstantGlobal("eraseNegative"); val eraseResumption = ConstantGlobal("eraseResumption"); val eraseFrames = ConstantGlobal("eraseFrames"); - val eraseString = ConstantGlobal("erasePositive"); val alloc = ConstantGlobal("alloc") val getPointer = ConstantGlobal("getPointer") diff --git a/effekt/shared/src/main/scala/effekt/machine/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/machine/PrettyPrinter.scala index 00131a929..0e3e86bfa 100644 --- a/effekt/shared/src/main/scala/effekt/machine/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/machine/PrettyPrinter.scala @@ -32,7 +32,6 @@ object PrettyPrinter extends ParenPrettyPrinter { case Type.Int() => "Int" case Type.Byte() => "Byte" case Type.Double() => "Double" - case Type.String() => "String" case Type.Reference(tpe) => toDoc(tpe) <> "*" } diff --git a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala b/effekt/shared/src/main/scala/effekt/machine/Transformer.scala index 86756b84c..12175eb18 100644 --- a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/machine/Transformer.scala @@ -382,7 +382,7 @@ object Transformer { } case core.Literal(javastring: String, _) => - val literal_binding = Variable(freshName("utf8StringLiteral"), Type.String()); + val literal_binding = Variable(freshName("utf8StringLiteral"), builtins.StringType); Binding { k => LiteralUTF8String(literal_binding, javastring.getBytes("utf-8"), k(literal_binding)) } @@ -477,7 +477,7 @@ object Transformer { case core.Type.TByte => Type.Byte() case core.Type.TBoolean => builtins.BooleanType case core.Type.TDouble => Type.Double() - case core.Type.TString => Type.String() + case core.Type.TString => Positive() case core.ValueType.Data(symbol, targs) => Positive() } diff --git a/effekt/shared/src/main/scala/effekt/machine/Tree.scala b/effekt/shared/src/main/scala/effekt/machine/Tree.scala index 8f1020665..a48f4547d 100644 --- a/effekt/shared/src/main/scala/effekt/machine/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/machine/Tree.scala @@ -222,7 +222,6 @@ enum Type { case Int() case Byte() case Double() - case String() case Reference(tpe: Type) } export Type.{ Positive, Negative } @@ -241,5 +240,7 @@ object builtins { val False: Tag = 0 val BooleanType = Positive() + val StringType = Positive() + val SingletonRecord: Tag = 0 } diff --git a/libraries/llvm/main.c b/libraries/llvm/main.c index f11320541..49e0e5198 100644 --- a/libraries/llvm/main.c +++ b/libraries/llvm/main.c @@ -41,4 +41,5 @@ int main(int argc, char *argv[]) { uv_loop_t *loop = uv_default_loop(); uv_run(loop, UV_RUN_DEFAULT); uv_loop_close(loop); + return 0; }