diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 64eb442c239a..3fe28dd79192 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1349,6 +1349,78 @@ object Parsers { else literal(inTypeOrSingleton = true) + /** Dedent a string literal by removing common leading whitespace. + * The amount of whitespace to remove is determined by the indentation + * of the last line (which should contain only whitespace before the + * closing delimiter). + * + * @param str The string content to dedent + * @param offset The source offset where the string literal begins + * @return The dedented string, or str if errors were reported + */ + private def dedentString(str: String, + offset: Offset, + closingIndent: String, + isFirstPart: Boolean, + isLastPart: Boolean): String = { + // Just explicitly do nothing when the `closingIndent` is empty. This is easier than trying + // to ensure that handling of the various `linesIterator`/`linesWithSeparators`/etc. + // APIs behaves predictably in the presence of empty leading/trailing lines + if (closingIndent == "") str + else { + if (closingIndent.contains('\t') && closingIndent.contains(' ')) { + syntaxError( + em"dedented string literal cannot mix tabs and spaces in indentation", + offset + ) + return str + } + + val linesAndWithSeps = (str.linesIterator.zip(str.linesWithSeparators)).toSeq + var lineOffset = offset + // start counting error location offsets only after opening delimiter + while(in.buf(lineOffset) == '\'') lineOffset += 1 + + def dedentLine(line: String, lineWithSep: String) = { + val result = + if (line.startsWith(closingIndent)) line.substring(closingIndent.length) + else if (line.trim.isEmpty) "" // Empty or whitespace-only lines + else { + syntaxError( + em"line in dedented string literal must be indented at least as much as the closing delimiter with an identical prefix", + lineOffset + ) + line + } + lineOffset += lineWithSep.length // Make sure to include any \n, \r, \r\n, or \n\r + result + } + + // If this is the first part of a string, then the first line is the empty string following + // the opening `'''` delimiter, so we skip it. If not, then the first line is immediately + // following an interpolated value, and should be used raw without indenting + val firstLine = { + val (line, lineWithSep) = linesAndWithSeps.head + lineOffset += lineWithSep.length + if (isFirstPart) Nil else Seq(line) + } + + // Process all lines except the first and last, which require special handling + val dedented = linesAndWithSeps.drop(1).dropRight(1).map { case (line, lineWithSep) => + dedentLine(line, lineWithSep) + } + + // If this is the last part of the string, then the last line is the indentation-only + // line preceding the closing delimiter, and should be ignored. If not, then the last line + // also needs to be de-dented + val lastLine = + if (isLastPart) Nil + else Seq(dedentLine(linesAndWithSeps.last._1, linesAndWithSeps.last._2)) + + (firstLine ++ dedented ++ lastLine).mkString("\n") + } + } + /** Literal ::= SimpleLiteral * | processedStringLiteral * | symbolLiteral @@ -1377,7 +1449,15 @@ object Parsers { case FLOATLIT => floatFromDigits(digits) case DOUBLELIT | DECILIT | EXPOLIT => doubleFromDigits(digits) case CHARLIT => in.strVal.head - case STRINGLIT | STRINGPART => in.strVal + case STRINGLIT | STRINGPART => + // Check if this is a dedented string (non-interpolated) + // For non-interpolated dedented strings, check if the token starts with ''' + val str = in.strVal + if (token == STRINGLIT && !inStringInterpolation && isDedentedStringLiteral(negOffset)) { + val (closingIdent, succeeded) = extractClosingIndent(str, negOffset) + if (succeeded) dedentString(str, negOffset, closingIdent, true, true) + else str + } else str case TRUE => true case FALSE => false case NULL => null @@ -1391,6 +1471,15 @@ object Parsers { Literal(Constant(value)) } + /** Check if a string literal at the given offset is a dedented string */ + def isDedentedStringLiteral(offset: Int): Boolean = { + val buf = in.buf + offset + 2 < buf.length && + buf(offset) == '\'' && + buf(offset + 1) == '\'' && + buf(offset + 2) == '\'' + } + if (inStringInterpolation) { val t = in.token match { case STRINGLIT | STRINGPART => @@ -1447,40 +1536,109 @@ object Parsers { in.charOffset + 1 < in.buf.length && in.buf(in.charOffset) == '"' && in.buf(in.charOffset + 1) == '"' + + val isDedented = + in.charOffset + 2 < in.buf.length && + in.buf(in.charOffset - 1) == '\'' && + in.buf(in.charOffset) == '\'' && + in.buf(in.charOffset + 1) == '\'' + in.nextToken() - def nextSegment(literalOffset: Offset) = - segmentBuf += Thicket( - literal(literalOffset, inPattern = inPattern, inStringInterpolation = true), - atSpan(in.offset) { - if (in.token == IDENTIFIER) - termIdent() - else if (in.token == USCORE && inPattern) { - in.nextToken() - Ident(nme.WILDCARD) - } - else if (in.token == THIS) { - in.nextToken() - This(EmptyTypeIdent) - } - else if (in.token == LBRACE) - if (inPattern) Block(Nil, inBraces(pattern())) - else expr() - else { - report.error(InterpolatedStringError(), source.atSpan(Span(in.offset))) - EmptyTree - } - }) - var offsetCorrection = if isTripleQuoted then 3 else 1 - while (in.token == STRINGPART) - nextSegment(in.offset + offsetCorrection) + val stringParts = new ListBuffer[(String, Offset)] + val interpolatedExprs = new ListBuffer[Tree] + + var offsetCorrection = if (isDedented) 3 else if (isTripleQuoted) 3 else 1 + while (in.token == STRINGPART) { + val literalOffset = in.offset + offsetCorrection + stringParts += ((in.strVal, literalOffset)) offsetCorrection = 0 - if (in.token == STRINGLIT) - segmentBuf += literal(inPattern = inPattern, negOffset = in.offset + offsetCorrection, inStringInterpolation = true) + in.nextToken() + + interpolatedExprs += atSpan(in.offset) { + if (in.token == IDENTIFIER) + termIdent() + else if (in.token == USCORE && inPattern) { + in.nextToken() + Ident(nme.WILDCARD) + } + else if (in.token == THIS) { + in.nextToken() + This(EmptyTypeIdent) + } + else if (in.token == LBRACE) + if (inPattern) Block(Nil, inBraces(pattern())) + else expr() + else { + report.error(InterpolatedStringError(), source.atSpan(Span(in.offset))) + EmptyTree + } + } + } + + val finalLiteral = if (in.token == STRINGLIT) { + val s = in.strVal + val off = in.offset + offsetCorrection + stringParts += ((s, off)) + in.nextToken() + true + } else false + + val dedentedParts = + if (!isDedented || stringParts.isEmpty) stringParts + else { + val lastPart = stringParts.last._1 + val (closingIndent, succeeded) = extractClosingIndent(lastPart, in.offset) + stringParts.zipWithIndex.map { case ((str, offset), index) => + val dedented = dedentString(str, in.offset, closingIndent, index == 0, index == stringParts.length - 1) + (dedented, offset) + } + } + + for ((str, expr) <- dedentedParts.zip(interpolatedExprs)) { + val (dedentedStr, offset) = str + segmentBuf += Thicket( + atSpan(offset, offset, offset + dedentedStr.length) { Literal(Constant(dedentedStr)) }, + expr + ) + } + + if (finalLiteral) { // Add the final literal if present + val (dedentedStr, offset) = dedentedParts.last + segmentBuf += atSpan(offset, offset, offset + dedentedStr.length) { Literal(Constant(dedentedStr)) } + } InterpolatedString(interpolator, segmentBuf.toList) } + /** Extract the closing indentation from the last line of a string */ + private def extractClosingIndent(str: String, offset: Offset): (String, Boolean) = { + // If the last line is empty, `linesIterator` and `linesWithSeparators` skips + // the empty string, so we must recognize that case and explicitly default to "" + // otherwise things will blow up + val closingIndent = str + .linesIterator + .zip(str.linesWithSeparators) + .toSeq + .lastOption + .filter((line, lineWithSep) => line == lineWithSep) + .map(_._1) + .getOrElse("") + + if (closingIndent.exists(!_.isWhitespace)) { + var lineOffset = offset + // start counting error location offsets only after opening delimiter + while(in.buf(lineOffset) == '\'') lineOffset += 1 + syntaxError( + em"last line of dedented string literal must contain only whitespace before closing delimiter", + lineOffset + str.linesWithSeparators.toList.dropRight(1).map(_.size).sum + ) + (str, false) + } else { + (closingIndent, true) + } + } + /* ------------- NEW LINES ------------------------------------------------- */ def newLineOpt(): Unit = diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 52e03de60dea..6574471a15f7 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -374,6 +374,7 @@ object Scanners { case STRINGLIT => currentRegion match { case InString(_, outer) => currentRegion = outer + case InDedentedString(_, outer) => currentRegion = outer case _ => } case _ => @@ -385,6 +386,9 @@ object Scanners { lastOffset = lastCharOffset currentRegion match case InString(multiLine, _) if lastToken != STRINGPART => fetchStringPart(multiLine) + case InDedentedString(quoteCount, _) if lastToken != STRINGPART => + offset = charOffset - 1 + getDedentedStringPartWithDelimiter(quoteCount, isInterpolated = true) case _ => fetchToken() if token == ERROR then adjustSepRegions(STRINGLIT) // make sure we exit enclosing string literal else @@ -888,6 +892,14 @@ object Scanners { getIdentRest() if (ch == '"' && token == IDENTIFIER) token = INTERPOLATIONID + else if (ch == '\'' && token == IDENTIFIER) + // Check for ''' to support dedented string interpolation + val la = lookaheadReader() + la.nextChar() + if (la.ch == '\'') + la.nextChar() + if (la.ch == '\'') + token = INTERPOLATIONID case '<' => // is XMLSTART? def fetchLT() = { val last = if (charOffset >= 2) buf(charOffset - 2) else ' ' @@ -978,7 +990,19 @@ object Scanners { case '\'' => def fetchSingleQuote(): Unit = { nextChar() - if isIdentifierStart(ch) then + if (ch == '\'') { // Check for triple single quote (dedented string literal) + nextChar() + if (ch == '\'') { + // We have at least ''' check if this is an interpolated dedented string + if (token == INTERPOLATIONID) { + nextRawChar() + val quoteCount = getDedentedString(isInterpolated = true) + currentRegion = InDedentedString(quoteCount, currentRegion) + } else getDedentedString(isInterpolated = false) + } + else error(em"empty character literal") // We have '' followed by something else + } + else if isIdentifierStart(ch) then charLitOr { getIdentRest(); QUOTEID } else if isOperatorPart(ch) && ch != '\\' then charLitOr { getOperatorRest(); QUOTEID } @@ -1255,6 +1279,118 @@ object Scanners { else error(em"unclosed string literal") } + private def getDedentedString(isInterpolated: Boolean): Int = { + // For interpolated strings, we're already at the first character after ''' + // For non-interpolated, we need to consume the first character + if (!isInterpolated) nextChar() + + // Count opening quotes (already consumed 3) + var quoteCount = 3 + while (ch == '\'') { + quoteCount += 1 + if (isInterpolated) nextRawChar() else nextChar() + } + + // Must be followed by a newline + if (ch != LF && ch != CR) { + error(em"dedented string literal must start with newline after opening quotes") + token = ERROR + return 0 + } + + // Collect all content using the string part parser + getDedentedStringPartWithDelimiter(quoteCount, isInterpolated) + + quoteCount + } + + @tailrec private def getDedentedStringPartWithDelimiter(quoteCount: Int, isInterpolated: Boolean): Unit = + // Check for closing delimiter with correct quote count + if (ch == '\'') { + // Count the quotes we encounter + var foundQuotes = 0 + while (ch == '\'' && foundQuotes < quoteCount + 1) { + foundQuotes += 1 + nextRawChar() + } + + if (foundQuotes == quoteCount && ch != '\'') { + // The while-loop above steps forward to the first non-`'` character, + // so we need to backtrack 1 char to avoid consuming it + charOffset -= 1 + // Found closing delimiter - exact match and not followed by another quote + setStrVal() + nextChar() // Switch from raw mode to normal mode + token = STRINGLIT + } else { + // Not the closing delimiter, add the quotes we found to content + for (_ <- 0 until foundQuotes) putChar('\'') + getDedentedStringPartWithDelimiter(quoteCount, isInterpolated) + } + } + else if (isInterpolated && ch == '$') { + if (handleStringInterpolation('\'')) { + getDedentedStringPartWithDelimiter(quoteCount, isInterpolated) + } + } + else { + val isUnclosedLiteral = !isUnicodeEscape && ch == SU + if (isUnclosedLiteral) incompleteInputError(em"unclosed dedented string literal") + else { + putChar(ch) + nextRawChar() + getDedentedStringPartWithDelimiter(quoteCount, isInterpolated) + } + } + end getDedentedStringPartWithDelimiter + + private def handleStringInterpolation(escapeChar: Char): Boolean = { + def getInterpolatedIdentRest(hasSupplement: Boolean): Unit = + @tailrec def loopRest(): Unit = + if ch != SU && isUnicodeIdentifierPart(ch) then + putChar(ch) ; nextRawChar() + loopRest() + else if atSupplementary(ch, isUnicodeIdentifierPart) then + putChar(ch) ; nextRawChar() + putChar(ch) ; nextRawChar() + loopRest() + else + finishNamedToken(IDENTIFIER, target = next) + end loopRest + setStrVal() + token = STRINGPART + next.lastOffset = charOffset - 1 + next.offset = charOffset - 1 + putChar(ch) ; nextRawChar() + if hasSupplement then + putChar(ch) ; nextRawChar() + loopRest() + end getInterpolatedIdentRest + + nextRawChar() + if (ch == '$' || ch == escapeChar) { + putChar(ch) + nextRawChar() + true // continue parsing + } + else if (ch == '{') { + setStrVal() + token = STRINGPART + false // don't continue, we're done with this string part + } + else if isUnicodeIdentifierStart(ch) || ch == '_' then + getInterpolatedIdentRest(hasSupplement = false) + false // don't continue, identifier rest handles it + else if atSupplementary(ch, isUnicodeIdentifierStart) then + getInterpolatedIdentRest(hasSupplement = true) + false // don't continue, identifier rest handles it + else + val escapeDesc = if escapeChar == '"' then "`$\"`, " else "`$'`, " + error(s"invalid string interpolation: `$$$$`, $escapeDesc`$$`ident or `$$`BlockExpr expected".toMessage, off = charOffset - 2) + putChar('$') + true // continue parsing after error + } + private def getRawStringLit(): Unit = if (ch == '\"') { nextRawChar() @@ -1299,46 +1435,9 @@ object Scanners { getStringPart(multiLine) } else if (ch == '$') { - def getInterpolatedIdentRest(hasSupplement: Boolean): Unit = - @tailrec def loopRest(): Unit = - if ch != SU && isUnicodeIdentifierPart(ch) then - putChar(ch) ; nextRawChar() - loopRest() - else if atSupplementary(ch, isUnicodeIdentifierPart) then - putChar(ch) ; nextRawChar() - putChar(ch) ; nextRawChar() - loopRest() - else - finishNamedToken(IDENTIFIER, target = next) - end loopRest - setStrVal() - token = STRINGPART - next.lastOffset = charOffset - 1 - next.offset = charOffset - 1 - putChar(ch) ; nextRawChar() - if hasSupplement then - putChar(ch) ; nextRawChar() - loopRest() - end getInterpolatedIdentRest - - nextRawChar() - if (ch == '$' || ch == '"') { - putChar(ch) - nextRawChar() + if (handleStringInterpolation('"')) { getStringPart(multiLine) } - else if (ch == '{') { - setStrVal() - token = STRINGPART - } - else if isUnicodeIdentifierStart(ch) || ch == '_' then - getInterpolatedIdentRest(hasSupplement = false) - else if atSupplementary(ch, isUnicodeIdentifierStart) then - getInterpolatedIdentRest(hasSupplement = true) - else - error("invalid string interpolation: `$$`, `$\"`, `$`ident or `$`BlockExpr expected".toMessage, off = charOffset - 2) - putChar('$') - getStringPart(multiLine) } else { val isUnclosedLiteral = !isUnicodeEscape && (ch == SU || (!multiLine && (ch == CR || ch == LF))) @@ -1662,6 +1761,7 @@ object Scanners { private def delimiter = this match case _: InString => "}(in string)" + case _: InDedentedString => "}(in dedented string)" case InParens(LPAREN, _) => ")" case InParens(LBRACKET, _) => "]" case _: InBraces => "}" @@ -1677,6 +1777,7 @@ object Scanners { end Region case class InString(multiLine: Boolean, outer: Region) extends Region(RBRACE) + case class InDedentedString(quoteCount: Int, outer: Region) extends Region(RBRACE) case class InParens(prefix: Token, outer: Region) extends Region(prefix + 1) case class InBraces(outer: Region) extends Region(RBRACE) case class InCase(outer: Region) extends Region(OUTDENT) diff --git a/tests/neg/dedented-string-literals.check b/tests/neg/dedented-string-literals.check new file mode 100644 index 000000000000..e760a5f4f1d7 --- /dev/null +++ b/tests/neg/dedented-string-literals.check @@ -0,0 +1,20 @@ +-- Error: tests/neg/dedented-string-literals.scala:4:27 ---------------------------------------------------------------- +4 | val noNewlineAfterOpen = '''content on same line // error: dedented string literal must start with newline after opening quotes + | ^ + | dedented string literal must start with newline after opening quotes +-- Error: tests/neg/dedented-string-literals.scala:7:0 ----------------------------------------------------------------- +7 |content // error: line in dedented string literal must be indented at least as much as the closing delimiter with an identical prefix + |^ + |line in dedented string literal must be indented at least as much as the closing delimiter with an identical prefix +-- Error: tests/neg/dedented-string-literals.scala:11:0 ---------------------------------------------------------------- +11 | tab line // error: line in dedented string literal must be indented at least as much as the closing delimiter with an identical prefix + |^ + |line in dedented string literal must be indented at least as much as the closing delimiter with an identical prefix +-- Error: tests/neg/dedented-string-literals.scala:17:0 ---------------------------------------------------------------- +17 | text''' // error: last line of dedented string literal must contain only whitespace before closing delimiter + |^ + |last line of dedented string literal must contain only whitespace before closing delimiter +-- Error: tests/neg/dedented-string-literals.scala:21:17 --------------------------------------------------------------- +21 | val unclosed = ''' // error: unclosed dedented string literal + | ^ + | dedented string literal must start with newline after opening quotes diff --git a/tests/neg/dedented-string-literals.scala b/tests/neg/dedented-string-literals.scala new file mode 100644 index 000000000000..f04e4aeda8b9 --- /dev/null +++ b/tests/neg/dedented-string-literals.scala @@ -0,0 +1,22 @@ +// Test error cases for dedented string literals + +object DedentedStringErrors { + val noNewlineAfterOpen = '''content on same line // error: dedented string literal must start with newline after opening quotes + + val notIndentedEnough = ''' +content // error: line in dedented string literal must be indented at least as much as the closing delimiter with an identical prefix + ''' + + val mixedTabsSpaces = ''' + tab line // error: line in dedented string literal must be indented at least as much as the closing delimiter with an identical prefix + space line + ''' + + val nonWhitespaceBeforeClosing = ''' + content here + text''' // error: last line of dedented string literal must contain only whitespace before closing delimiter +} + +object UnclosedTest { + val unclosed = ''' // error: unclosed dedented string literal + some content diff --git a/tests/run/dedented-string-literals.check b/tests/run/dedented-string-literals.check new file mode 100644 index 000000000000..06604141dea8 --- /dev/null +++ b/tests/run/dedented-string-literals.check @@ -0,0 +1,131 @@ +Basic: +i am cow +hear me moo +---- +No Indent: + +i am cow +hear me moo + +---- +With indent: + i am cow + hear me moo +---- +Empty: +[] +---- +Single line: +hello world +---- +Blank lines: +line 1 + +line 3 +---- +Deep indent: + deeply + indented + content +---- +Mixed indent: + first level + second level + third level +---- +With triple quotes: +''' +i am cow +''' +---- +Extended 5 quotes: +'''' +content with four quotes +'''' +---- +Normalized newlines: +Has only LF: true +---- +Special chars: +!"#$%&()*+,-./:;<=>?@[\]^_`{|}~ +---- +Unicode: +Hello 世界 +---- +With tabs: + tab indented + content here +---- +Empty lines anywhere: +[ +content + +more content +] +---- +With quotes: +"double quotes" +'single quote' +'' +---- +Zero indent: +content +---- +Precise: +Length: 9 +Content: [ ab + cd] +Chars: List( , , a, b, +, , , c, d) +---- +Interpolated: +Hello Alice +You are 30 years old +---- +Escaped Interpolated: +Hello $name +You are $age years old +---- +Formatted: +Value: 00042 +Done +---- +Pattern matching: +Pattern result: matched basic +---- +Interpolated pattern: +Interpolated pattern result: matched greeting +---- +Pattern matching (two lines): +Two line pattern result: matched two lines +---- +Interpolated pattern (two lines): +Two line interpolated result: matched two line greeting +---- +In function: + function content + more content +---- +In class: + class member + content +---- +In list: +Item: [ first] +Item: [ second] +Item: [ third] +---- +Nested in expressions: +prefix middlesuffix +---- +Type ascription: +Value: [ first line + indented line + third line] +Type matches: true +---- +valueOf test: +Value: [ alpha + beta + gamma] +Type matches: true diff --git a/tests/run/dedented-string-literals.scala b/tests/run/dedented-string-literals.scala new file mode 100644 index 000000000000..643c83293a30 --- /dev/null +++ b/tests/run/dedented-string-literals.scala @@ -0,0 +1,288 @@ +// Test runtime behavior of dedented string literals + +object Test { + def main(args: Array[String]): Unit = { + val basic = ''' + i am cow + hear me moo + ''' + println("Basic:") + println(basic) + println("----") + + val noIndent = ''' +i am cow +hear me moo +''' + println("No Indent:") + println(noIndent) + println("----") + + val withIndentPreserved = ''' + i am cow + hear me moo + ''' + println("With indent:") + println(withIndentPreserved) + println("----") + + val empty = ''' + ''' + println("Empty:") + println(s"[${empty}]") + println("----") + + val singleLine = ''' + hello world + ''' + println("Single line:") + println(singleLine) + println("----") + + val blankLines = ''' + line 1 + + line 3 + ''' + println("Blank lines:") + println(blankLines) + println("----") + + val deepIndent = ''' + deeply + indented + content + ''' + println("Deep indent:") + println(deepIndent) + println("----") + + val mixedIndent = ''' + first level + second level + third level + ''' + println("Mixed indent:") + println(mixedIndent) + println("----") + + val withTripleQuotes = '''' + ''' + i am cow + ''' + '''' + println("With triple quotes:") + println(withTripleQuotes) + println("----") + + val extended5 = ''''' + '''' + content with four quotes + '''' + ''''' + println("Extended 5 quotes:") + println(extended5) + println("----") + + val normalized = ''' + line1 + line2 + ''' + println("Normalized newlines:") + println(s"Has only LF: ${!normalized.contains('\r')}") + println("----") + + val specialChars = ''' + !"#$%&()*+,-./:;<=>?@[\]^_`{|}~ + ''' + println("Special chars:") + println(specialChars) + println("----") + + val unicode = ''' + Hello 世界 + ''' + println("Unicode:") + println(unicode) + println("----") + + val withTabs = ''' + tab indented + content here + ''' + println("With tabs:") + println(withTabs) + println("----") + + val emptyLinesAnywhere = ''' + + content + + more content + + ''' + println("Empty lines anywhere:") + println(s"[${emptyLinesAnywhere}]") + println("----") + + val withQuotes = ''' + "double quotes" + 'single quote' + '' + ''' + println("With quotes:") + println(withQuotes) + println("----") + + val zeroIndent = ''' + content + ''' + println("Zero indent:") + println(zeroIndent) + println("----") + + val precise = ''' + ab + cd + ''' + println("Precise:") + println(s"Length: ${precise.length}") + println(s"Content: [${precise}]") + println(s"Chars: ${precise.toList}") + println("----") + + val name = "Alice" + val age = 30 + val interpolated = s''' + Hello $name + You are $age years old + ''' + println("Interpolated:") + println(interpolated) + println("----") + + val escapedInterpolated = s''' + Hello $$name + You are $$age years old + ''' + println("Escaped Interpolated:") + println(escapedInterpolated) + println("----") + + val value = 42 + val formatted = f''' + Value: $value%05d + Done + ''' + println("Formatted:") + println(formatted) + println("----") + + def testPattern(s: String): String = s match { + case ''' + test + ''' => "matched basic" + case ''' + other + ''' => "matched other" + case _ => "no match" + } + println("Pattern matching:") + println(s"Pattern result: ${testPattern("test")}") + println("----") + + def testInterpolatedPattern(s: String): String = s match { + case s''' + Hello $_ + ''' => "matched greeting" + case _ => "no match" + } + println("Interpolated pattern:") + println(s"Interpolated pattern result: ${testInterpolatedPattern("Hello World")}") + println("----") + + def testPatternTwoLines(s: String): String = s match { + case ''' + line one + line two + ''' => "matched two lines" + case _ => "no match" + } + println("Pattern matching (two lines):") + println(s"Two line pattern result: ${testPatternTwoLines("line one\nline two")}") + println("----") + + def testInterpolatedPatternTwoLines(s: String): String = s match { + case s''' + First: $_ + Second: $_ + ''' => "matched two line greeting" + case _ => "no match" + } + println("Interpolated pattern (two lines):") + println(s"Two line interpolated result: ${testInterpolatedPatternTwoLines("First: Alice\nSecond: Bob")}") + println("----") + + def inFunction = ''' + function content + more content + ''' + println("In function:") + println(inFunction) + println("----") + + class InClass { + val inClass = ''' + class member + content + ''' + } + val classInstance = new InClass + println("In class:") + println(classInstance.inClass) + println("----") + + val list = List( + ''' + first + ''', + ''' + second + ''', + ''' + third + ''' + ) + println("In list:") + list.foreach { item => + println(s"Item: [$item]") + } + println("----") + + val nested = "prefix" + ''' + middle + ''' + "suffix" + println("Nested in expressions:") + println(nested) + println("----") + + val typedVal: ''' + first line + indented line + third line + ''' = " first line\n indented line\n third line" + println("Type ascription:") + println(s"Value: [$typedVal]") + println(s"Type matches: ${typedVal == " first line\n indented line\n third line"}") + println("----") + + val valueOfResult = scala.compiletime.constValue[''' + alpha + beta + gamma + '''] + println("valueOf test:") + println(s"Value: [$valueOfResult]") + println(s"Type matches: ${valueOfResult == " alpha\n beta\n gamma"}") + } +}