1

I'm trying to port the Scala track of Exercism to Scala 3. Currently stuck on the "testgen" subproject, as I have next to no experience of reflections and macros. This Scala 2 method seems to ensure that a String can be coerced to a literal constant before stringifying back the result?

It is used in the KinderGardenTestGenerator and WordCountTestGenerator, presumably to sanitize student input?

So I want to replace it in Scala 3 with something like

def escape(raw: String): String = {
  Literal(StringConstant(Expr(e))).toString
}

It seems to get access to the reflect methods you need a (using Quotes) and to do that you need to use inline.

The closest I've gotten to a solution is this, splitting out the methods into their own object:

import scala.quoted.*

object Escaper {
  inline def escape(inline raw: String): String = ${literalize('{raw})}

  def literalize(e: Expr[String])(using Quotes): Expr[String] = {
    import quotes.reflect.*
     Expr(Literal(StringConstant(e.valueOrAbort)).toString)
  }
}

It seem to compile, but fails once it reaches compiling KinderGardenTestGenerator, where I get the following error:

[error] -- Error: /home/larsw/projects/scala/testgen/src/main/scala/KindergartenGardenTestGenerator.scala:33:47 
[error] 33 |              s"""Garden.defaultGarden(${escape(diagram.toString)}).$property("$student")"""
[error]    |                                         ^^^^^^^^^^^^^^^^^^^^^^^^
[error]    |                      Could not find class testgen.Escaper$ in classpath
[error]    |----------------------------------------------------------------------------
[error]    |Inline stack trace
[error]    |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error]    |This location contains code that was inlined from Escaper.scala:8
[error]  8 |  inline def escape(inline raw: String): String = ${literalize('{raw})}
[error]    |                                                  ^^^^^^^^^^^^^^^^^^^^^
[error]     ----------------------------------------------------------------------------
[error] -- Error: /home/larsw/projects/scala/testgen/src/main/scala/WordCountTestGenerator.scala:25:37 
[error] 25 |        val sentence = Escaper.escape(args("sentence").toString)
[error]    |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[error]    |                      Could not find class testgen.Escaper$ in classpath
[error]    |----------------------------------------------------------------------------
[error]    |Inline stack trace
[error]    |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error]    |This location contains code that was inlined from Escaper.scala:8
[error]  8 |  inline def escape(inline raw: String): String = ${literalize('{raw})}
[error]    |                                                  ^^^^^^^^^^^^^^^^^^^^^
[error]     ----------------------------------------------------------------------------

And it feels like overkill to inline, my use case isn't that advanced, I don't need to generate code. My questions are:

  1. Is there a way to Literalize and sanitize the strings without macros or reflection?
  2. Or is there some way to access the reflection methods without inline and (using Quotes)
  3. Or if my proposed solution is the only way - Why does it not find it on the classpath, even though import seems to work?

(Also interested in links to good videos or courses teaching Scala 3 macros. I'm eager to learn more and is very excited about the possibilities here, especially since Exercism are adding "representers" that can give students and mentors feedback and improvement suggestions for solutions, which looks like a great fit for macros.)

2
  • 1
    Try classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat Commented Sep 4, 2022 at 21:04
  • Did not help unfortunately, but I learned a new setting, thanks! Commented Sep 5, 2022 at 5:46

1 Answer 1

2

This Scala 2 method seems to ensure that a String can be coerced to a literal constant before stringifying back the result?

No, this is just an exotic way to transform \n into \\n etc.

Scala: How can I get an escaped representation of a string?

Your Scala 3 macro now does the wrong job. Try Simão Martins's answer from there

import scala.quoted.*

inline def escape(inline raw: String): String = ${escapeImpl('{raw})}

def escapeImpl(raw: Expr[String])(using Quotes): Expr[String] =
  import quotes.reflect.*
  Literal(StringConstant(raw.show)).asExprOf[String]

I guess a Scala 3 macro is now an overkill. Just try to escape with a different implemenation from there, e.g. 0__'s

def escape (s: String): String = "\"" + escape0(s) + "\""
def escape0(s: String): String = s.flatMap(escapedChar)

def escapedChar(ch: Char): String = ch match {
  case '\b' => "\\b"
  case '\t' => "\\t"
  case '\n' => "\\n"
  case '\f' => "\\f"
  case '\r' => "\\r"
  case '"'  => "\\\""
  case '\'' => "\\\'"
  case '\\' => "\\\\"
  case _    => if (ch.isControl) "\\0" + Integer.toOctalString(ch.toInt) 
               else              String.valueOf(ch)
}
Sign up to request clarification or add additional context in comments.

1 Comment

Yep, fixed it and answered question 1, thank you Dmytro. To answer myself for question 2 - no, macros are by definition compile time, that is why you need a "inline" entry point which then provides the Quotes implicit for you when the compiler stage runs. Question 3 remains, I have now a new error - Exception occurred while executing macro expansion. scala.quoted.runtime.impl.ScopeException: Expression created in a splice was used outside of that splice. Created in: play-json/shared/src/main/scala-3/play/api/libs/json/JsMacroImpl.scala:431 at column 13 But I'll look into that myself

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.