@@ -986,10 +986,6 @@ object Scanners {
986986 }
987987 fetchDoubleQuote()
988988 case '\' ' =>
989- def dedentedStringPart () = {
990- getDedentedString(isInterpolated = true )
991- currentRegion = InDedentedString (currentRegion)
992- }
993989 def fetchSingleQuote (): Unit = {
994990 nextChar()
995991 // Check for triple single quote (dedented string literal)
@@ -1001,7 +997,8 @@ object Scanners {
1001997 if (token == INTERPOLATIONID ) {
1002998 // For interpolation, handle as string part
1003999 nextRawChar()
1004- dedentedStringPart()
1000+ getDedentedString(isInterpolated = true )
1001+ currentRegion = InDedentedString (currentRegion)
10051002 } else {
10061003 getDedentedString(isInterpolated = false )
10071004 }
@@ -1300,177 +1297,182 @@ object Scanners {
13001297 * @param isInterpolated If true, handles $ interpolation and returns STRINGPART tokens
13011298 */
13021299 private def getDedentedString (isInterpolated : Boolean ): Unit = {
1300+ // For interpolated strings, we're already at the first character after '''
1301+ // For non-interpolated, we need to consume the first character
1302+ if (! isInterpolated) nextChar()
1303+
1304+ // Count opening quotes (already consumed 3)
1305+ var quoteCount = 3
1306+ while (ch == '\' ' ) {
1307+ quoteCount += 1
1308+ if (isInterpolated) nextRawChar() else nextChar()
1309+ }
1310+
1311+ // Must be followed by a newline
1312+ if (ch != LF && ch != CR ) {
1313+ error(em " dedented string literal must start with newline after opening quotes " )
1314+ token = ERROR
1315+ return
1316+ }
1317+
1318+ // Skip the initial newline (CR LF or just LF)
1319+ if (ch == CR ) nextRawChar()
1320+ if (ch == LF ) nextRawChar()
1321+
1322+ // For interpolated strings, check if we need to handle $ interpolation first
13031323 if (isInterpolated) {
1304- // For interpolated strings: parse incrementally, handling $ expressions
1305- getDedentedStringPartImpl()
1324+ getDedentedStringPartWithDelimiter(quoteCount)
13061325 } else {
1307- // For non-interpolated strings: parse entire string and dedent
1308- // Count opening quotes (already consumed 3)
1309- nextChar()
1310- var quoteCount = 3
1311- while (ch == '\' ' ) {
1312- quoteCount += 1
1313- nextChar()
1314- }
1326+ // Collect all lines until we find the closing delimiter
1327+ val lines = scala.collection.mutable.ArrayBuffer [String ]()
1328+ val lineIndents = scala.collection.mutable.ArrayBuffer [String ]()
1329+ var currentLine = new StringBuilder
1330+ var currentIndent = new StringBuilder
1331+ var atLineStart = true
1332+ var closingIndent : String = null
1333+ var foundClosing = false
1334+
1335+ while (! foundClosing && ch != SU ) {
1336+ if (atLineStart) {
1337+ // Collect indentation
1338+ currentIndent.clear()
1339+ while (ch == ' ' || ch == '\t ' ) {
1340+ currentIndent.append(ch)
1341+ nextRawChar()
1342+ }
13151343
1316- // Must be followed by a newline
1317- if (ch != LF && ch != CR ) {
1318- error(em " dedented string literal must start with newline after opening quotes " )
1319- token = ERROR
1320- } else {
1321- // Skip the initial newline (CR LF or just LF)
1322- if (ch == CR ) nextRawChar()
1323- if (ch == LF ) nextRawChar()
1324-
1325- // Collect all lines until we find the closing delimiter
1326- val lines = scala.collection.mutable.ArrayBuffer [String ]()
1327- val lineIndents = scala.collection.mutable.ArrayBuffer [String ]()
1328- var currentLine = new StringBuilder
1329- var currentIndent = new StringBuilder
1330- var atLineStart = true
1331- var closingIndent : String = null
1332- var foundClosing = false
1333-
1334- while (! foundClosing && ch != SU ) {
1335- if (atLineStart) {
1336- // Collect indentation
1337- currentIndent.clear()
1338- while (ch == ' ' || ch == '\t ' ) {
1339- currentIndent.append(ch)
1344+ // Check if this might be the closing delimiter
1345+ if (ch == '\' ' ) {
1346+ var endQuoteCount = 0
1347+ while (ch == '\' ' && endQuoteCount < quoteCount + 1 ) {
1348+ endQuoteCount += 1
13401349 nextRawChar()
13411350 }
13421351
1343- // Check if this might be the closing delimiter
1344- if (ch == '\' ' ) {
1345- var endQuoteCount = 0
1346- while (ch == '\' ' && endQuoteCount < quoteCount + 1 ) {
1347- endQuoteCount += 1
1348- nextRawChar()
1349- }
1350-
1351- if (endQuoteCount == quoteCount && ch != '\' ' ) {
1352- // Found closing delimiter (not followed by another quote)
1353- foundClosing = true
1354- closingIndent = currentIndent.toString
1355- } else {
1356- // False alarm, these quotes are part of the content
1357- // We need to restore and add them to current line
1358- currentLine.append(currentIndent)
1359- for (_ <- 0 until endQuoteCount) currentLine.append('\' ' )
1360- atLineStart = false
1361- }
1352+ if (endQuoteCount == quoteCount && ch != '\' ' ) {
1353+ // Found closing delimiter (not followed by another quote)
1354+ foundClosing = true
1355+ closingIndent = currentIndent.toString
13621356 } else {
1357+ // False alarm, these quotes are part of the content
1358+ // We need to restore and add them to current line
1359+ currentLine.append(currentIndent)
1360+ for (_ <- 0 until endQuoteCount) currentLine.append('\' ' )
13631361 atLineStart = false
13641362 }
1363+ } else {
1364+ atLineStart = false
13651365 }
1366+ }
13661367
1367- if (! foundClosing && ! atLineStart) {
1368- // Regular content
1369- if (ch == CR || ch == LF ) {
1370- // End of line
1371- lineIndents += currentIndent.toString
1372- lines += currentLine.toString
1373- currentLine.clear()
1374- currentIndent.clear()
1375-
1376- // Normalize newlines to \n
1377- if (ch == CR ) nextRawChar()
1378- if (ch == LF ) nextRawChar()
1379- atLineStart = true
1380- } else {
1381- currentLine.append(ch)
1382- nextRawChar()
1383- }
1368+ if (! foundClosing && ! atLineStart) {
1369+ // Regular content
1370+ if (ch == CR || ch == LF ) {
1371+ // End of line
1372+ lineIndents += currentIndent.toString
1373+ lines += currentLine.toString
1374+ currentLine.clear()
1375+ currentIndent.clear()
1376+
1377+ // Normalize newlines to \n
1378+ if (ch == CR ) nextRawChar()
1379+ if (ch == LF ) nextRawChar()
1380+ atLineStart = true
1381+ } else {
1382+ currentLine.append(ch)
1383+ nextRawChar()
13841384 }
13851385 }
1386+ }
13861387
1387- if (! foundClosing) {
1388- incompleteInputError(em " unclosed dedented string literal " )
1389- } else if (closingIndent == null ) {
1390- error(em " internal error: closing indent not set " )
1391- token = ERROR
1392- } else {
1393- // Validate and dedent all lines
1394- val dedentedLines = scala.collection.mutable.ArrayBuffer [String ]()
1395- val closingIndentLen = closingIndent.length
1396- var hasSpaces = false
1397- var hasTabs = false
1398-
1399- for (indent <- closingIndent) {
1400- if (indent == ' ' ) hasSpaces = true
1401- if (indent == '\t ' ) hasTabs = true
1402- }
1388+ if (! foundClosing) {
1389+ incompleteInputError(em " unclosed dedented string literal " )
1390+ } else if (closingIndent == null ) {
1391+ error(em " internal error: closing indent not set " )
1392+ token = ERROR
1393+ } else {
1394+ // Validate and dedent all lines
1395+ val dedentedLines = scala.collection.mutable.ArrayBuffer [String ]()
1396+ val closingIndentLen = closingIndent.length
1397+ var hasSpaces = false
1398+ var hasTabs = false
1399+
1400+ for (indent <- closingIndent) {
1401+ if (indent == ' ' ) hasSpaces = true
1402+ if (indent == '\t ' ) hasTabs = true
1403+ }
14031404
1404- var hasError = false
1405- for (i <- 0 until lines.length if ! hasError) {
1406- val line = lines(i)
1407- val indent = lineIndents(i)
1408-
1409- // Check for mixed tabs and spaces
1410- var lineHasSpaces = false
1411- var lineHasTabs = false
1412- for (ch <- indent) {
1413- if (ch == ' ' ) lineHasSpaces = true
1414- if (ch == '\t ' ) lineHasTabs = true
1415- }
1405+ var hasError = false
1406+ for (i <- 0 until lines.length if ! hasError) {
1407+ val line = lines(i)
1408+ val indent = lineIndents(i)
1409+
1410+ // Check for mixed tabs and spaces
1411+ var lineHasSpaces = false
1412+ var lineHasTabs = false
1413+ for (ch <- indent) {
1414+ if (ch == ' ' ) lineHasSpaces = true
1415+ if (ch == '\t ' ) lineHasTabs = true
1416+ }
14161417
1417- if ((hasSpaces && lineHasTabs) || (hasTabs && lineHasSpaces)) {
1418- error(em " dedented string literal cannot mix tabs and spaces in indentation " )
1418+ if ((hasSpaces && lineHasTabs) || (hasTabs && lineHasSpaces)) {
1419+ error(em " dedented string literal cannot mix tabs and spaces in indentation " )
1420+ token = ERROR
1421+ hasError = true
1422+ } else if (line.isEmpty) {
1423+ // Empty lines are allowed
1424+ dedentedLines += " "
1425+ } else {
1426+ // Non-empty lines must be indented at least as much as closing delimiter
1427+ if (! indent.startsWith(closingIndent)) {
1428+ error(em " line in dedented string literal must be indented at least as much as the closing delimiter " )
14191429 token = ERROR
14201430 hasError = true
1421- } else if (line.isEmpty) {
1422- // Empty lines are allowed
1423- dedentedLines += " "
14241431 } else {
1425- // Non-empty lines must be indented at least as much as closing delimiter
1426- if (! indent.startsWith(closingIndent)) {
1427- error(em " line in dedented string literal must be indented at least as much as the closing delimiter " )
1428- token = ERROR
1429- hasError = true
1430- } else {
1431- // Remove the closing indentation from this line
1432- dedentedLines += indent.substring(closingIndentLen) + line
1433- }
1432+ // Remove the closing indentation from this line
1433+ dedentedLines += indent.substring(closingIndentLen) + line
14341434 }
14351435 }
1436+ }
14361437
1437- if (! hasError) {
1438- // Set the string value (join with \n)
1439- strVal = dedentedLines.mkString(" \n " )
1440- litBuf.clear()
1441- token = STRINGLIT
1442- }
1438+ if (! hasError) {
1439+ // Set the string value (join with \n)
1440+ strVal = dedentedLines.mkString(" \n " )
1441+ litBuf.clear()
1442+ token = STRINGLIT
14431443 }
14441444 }
14451445 }
14461446 }
14471447
1448- /** For interpolated dedented strings - parse string content until ''' or $ */
1449- @ tailrec private def getDedentedStringPartImpl (): Unit =
1450- // Check for closing ''' delimiter
1448+ /** Parse interpolated dedented string content, handling $ expressions.
1449+ * This collects content until hitting $ or closing delimiter.
1450+ * Respects the quote count for extended delimiters.
1451+ *
1452+ * Note: This parses with the same format requirements as non-interpolated dedented strings
1453+ * (newline after opening, extended delimiters, etc.) but does NOT dedent the content during
1454+ * parsing. Dedenting for interpolated strings must be handled at runtime after all parts
1455+ * are assembled, similar to how the string interpolator combines the parts.
1456+ */
1457+ @ tailrec private def getDedentedStringPartWithDelimiter (quoteCount : Int ): Unit =
1458+ // Check for closing delimiter with correct quote count
14511459 if (ch == '\' ' ) {
1452- nextRawChar()
1453- if (ch == '\' ' ) {
1460+ // Count the quotes we encounter
1461+ var foundQuotes = 0
1462+ while (ch == '\' ' && foundQuotes < quoteCount + 1 ) {
1463+ foundQuotes += 1
14541464 nextRawChar()
1455- if (ch == '\' ' ) {
1456- // Found closing '''
1457- nextChar()
1458- // For now, set the string value without dedenting
1459- // TODO: implement proper dedenting for interpolated strings
1460- setStrVal()
1461- token = STRINGLIT
1462- }
1463- else {
1464- // Two quotes followed by something else, add them to content
1465- putChar('\' ' )
1466- putChar('\' ' )
1467- getDedentedStringPartImpl()
1468- }
14691465 }
1470- else {
1471- // Single quote followed by something else, add it to content
1472- putChar('\' ' )
1473- getDedentedStringPartImpl()
1466+
1467+ if (foundQuotes == quoteCount && ch != '\' ' ) {
1468+ // Found closing delimiter - exact match and not followed by another quote
1469+ nextChar()
1470+ setStrVal()
1471+ token = STRINGLIT
1472+ } else {
1473+ // Not the closing delimiter, add the quotes we found to content
1474+ for (_ <- 0 until foundQuotes) putChar('\' ' )
1475+ getDedentedStringPartWithDelimiter(quoteCount)
14741476 }
14751477 }
14761478 else if (ch == '$' ) {
@@ -1501,7 +1503,7 @@ object Scanners {
15011503 if (ch == '$' || ch == '\' ' ) {
15021504 putChar(ch)
15031505 nextRawChar()
1504- getDedentedStringPartImpl( )
1506+ getDedentedStringPartWithDelimiter(quoteCount )
15051507 }
15061508 else if (ch == '{' ) {
15071509 setStrVal()
@@ -1514,7 +1516,7 @@ object Scanners {
15141516 else
15151517 error(" invalid string interpolation: `$$`, `$'`, `$`ident or `$`BlockExpr expected" .toMessage, off = charOffset - 2 )
15161518 putChar('$' )
1517- getDedentedStringPartImpl( )
1519+ getDedentedStringPartWithDelimiter(quoteCount )
15181520 }
15191521 else {
15201522 val isUnclosedLiteral = ! isUnicodeEscape && ch == SU
@@ -1523,10 +1525,10 @@ object Scanners {
15231525 else {
15241526 putChar(ch)
15251527 nextRawChar()
1526- getDedentedStringPartImpl( )
1528+ getDedentedStringPartWithDelimiter(quoteCount )
15271529 }
15281530 }
1529- end getDedentedStringPartImpl
1531+ end getDedentedStringPartWithDelimiter
15301532
15311533 private def getRawStringLit (): Unit =
15321534 if (ch == '\" ' ) {
0 commit comments