Skip to content

Commit e65c5e6

Browse files
committed
Simplify Conversion
1 parent e41cb1c commit e65c5e6

File tree

2 files changed

+97
-93
lines changed

2 files changed

+97
-93
lines changed

compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala

Lines changed: 93 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import scala.util.matching.Regex.Match
99

1010
import java.util.{Calendar, Date, Formattable}
1111

12+
import PartialFunction.cond
13+
1214
/** Formatter string checker. */
1315
abstract class FormatChecker(using reporter: InterpolationReporter):
1416

@@ -19,7 +21,7 @@ abstract class FormatChecker(using reporter: InterpolationReporter):
1921
// count of args, for checking indexes
2022
def argc: Int
2123

22-
val allFlags = "-#+ 0,(<"
24+
// match a conversion specifier
2325
val formatPattern = """%(?:(\d+)\$)?([-#+ 0,(<]+)?(\d+)?(\.\d+)?([tT]?[%a-zA-Z])?""".r
2426

2527
// ordinal is the regex group index in the format pattern
@@ -95,18 +97,16 @@ abstract class FormatChecker(using reporter: InterpolationReporter):
9597
extension (inline value: Boolean)
9698
inline def or(inline body: => Unit): Boolean = value || { body ; false }
9799
inline def orElse(inline body: => Unit): Boolean = value || { body ; true }
98-
inline def but(inline body: => Unit): Boolean = value && { body ; false }
99100
inline def and(inline body: => Unit): Boolean = value && { body ; true }
101+
inline def but(inline body: => Unit): Boolean = value && { body ; false }
100102

101-
/** A conversion specifier matched in the argi'th string part,
102-
* with `argc` arguments to interpolate.
103-
*/
104-
sealed abstract class Conversion:
105-
// the match for this descriptor
106-
def descriptor: Match
107-
// the part number for reporting errors
108-
def argi: Int
103+
enum Kind:
104+
case StringXn, HashXn, BooleanXn, CharacterXn, IntegralXn, FloatingPointXn, DateTimeXn, LiteralXn, ErrorXn
105+
import Kind.*
109106

107+
/** A conversion specifier matched in the argi'th string part, with `argc` arguments to interpolate.
108+
*/
109+
final class Conversion(val descriptor: Match, val argi: Int, val kind: Kind):
110110
// the descriptor fields
111111
val index: Option[Int] = descriptor.intOf(Index)
112112
val flags: String = descriptor.stringOf(Flags)
@@ -115,26 +115,86 @@ abstract class FormatChecker(using reporter: InterpolationReporter):
115115
val op: String = descriptor.stringOf(CC)
116116

117117
// the conversion char is the head of the op string (but see DateTimeXn)
118-
val cc: Char = if isError then '?' else op(0)
118+
val cc: Char =
119+
kind match
120+
case ErrorXn => '?'
121+
case DateTimeXn => if op.length > 1 then op(1) else '?'
122+
case _ => op(0)
119123

120-
def isError: Boolean = false
121124
def isIndexed: Boolean = index.nonEmpty || hasFlag('<')
122-
def isLiteral: Boolean = false
125+
def isError: Boolean = kind == ErrorXn
126+
def isLiteral: Boolean = kind == LiteralXn
123127

124128
// descriptor is at index 0 of the part string
125129
def isLeading: Boolean = descriptor.at(Spec) == 0
126130

131+
// flags and index in specifier are ok
132+
private def goodies = goodFlags && goodIndex
133+
127134
// true if passes. Default checks flags and index
128-
def verify: Boolean = goodFlags && goodIndex
135+
def verify: Boolean =
136+
kind match {
137+
case StringXn => goodies
138+
case BooleanXn => goodies
139+
case HashXn => goodies
140+
case CharacterXn => goodies && noPrecision && only_-("c conversion")
141+
case IntegralXn =>
142+
def d_# = cc == 'd' && hasFlag('#') and badFlag('#', "# not allowed for d conversion")
143+
def x_comma = cc != 'd' && hasFlag(',') and badFlag(',', "',' only allowed for d conversion of integral types")
144+
goodies && noPrecision && !d_# && !x_comma
145+
case FloatingPointXn =>
146+
goodies && (cc match {
147+
case 'a' | 'A' =>
148+
val badFlags = ",(".filter(hasFlag)
149+
noPrecision && badFlags.isEmpty or badFlags.foreach(badf => badFlag(badf, s"'$badf' not allowed for a, A"))
150+
case _ => true
151+
})
152+
case DateTimeXn =>
153+
def hasCC = op.length == 2 or errorAt(CC)("Date/time conversion must have two characters")
154+
def goodCC = "HIklMSLNpzZsQBbhAaCYyjmdeRTrDFc".contains(cc) or errorAt(CC, 1)(s"'$cc' doesn't seem to be a date or time conversion")
155+
goodies && hasCC && goodCC && noPrecision && only_-("date/time conversions")
156+
case LiteralXn =>
157+
op match {
158+
case "%" => goodies && noPrecision and width.foreach(_ => warningAt(Width)("width ignored on literal"))
159+
case "n" => noFlags && noWidth && noPrecision
160+
}
161+
case ErrorXn =>
162+
errorAt(CC)(s"illegal conversion character '$cc'")
163+
false
164+
}
129165

130166
// is the specifier OK with the given arg
131-
def accepts(arg: ClassTag[?]): Boolean = true
167+
def accepts(arg: ClassTag[?]): Boolean =
168+
kind match
169+
case BooleanXn => arg == classTag[Boolean] orElse warningAt(CC)("Boolean format is null test for non-Boolean")
170+
case IntegralXn =>
171+
arg == classTag[BigInt] || !cond(cc) {
172+
case 'o' | 'x' | 'X' if hasAnyFlag("+ (") => "+ (".filter(hasFlag).foreach(bad => badFlag(bad, s"only use '$bad' for BigInt conversions to o, x, X")) ; true
173+
}
174+
case _ => true
132175

133176
// what arg type if any does the conversion accept
134-
def acceptableVariants: List[ClassTag[?]]
177+
def acceptableVariants: List[ClassTag[?]] =
178+
kind match {
179+
case StringXn => if hasFlag('#') then classTag[Formattable] :: Nil else classTag[Any] :: Nil
180+
case BooleanXn => classTag[Boolean] :: Conversion.FakeNullTag :: Nil
181+
case HashXn => classTag[Any] :: Nil
182+
case CharacterXn => classTag[Char] :: classTag[Byte] :: classTag[Short] :: classTag[Int] :: Nil
183+
case IntegralXn => classTag[Int] :: classTag[Long] :: classTag[Byte] :: classTag[Short] :: classTag[BigInt] :: Nil
184+
case FloatingPointXn => classTag[Double] :: classTag[Float] :: classTag[BigDecimal] :: Nil
185+
case DateTimeXn => classTag[Long] :: classTag[Calendar] :: classTag[Date] :: Nil
186+
case LiteralXn => Nil
187+
case ErrorXn => Nil
188+
}
135189

136-
// what flags does the conversion accept? defaults to all
137-
protected def okFlags: String = allFlags
190+
// what flags does the conversion accept?
191+
private def okFlags: String =
192+
kind match {
193+
case StringXn => "-#<"
194+
case BooleanXn | HashXn => "-<"
195+
case LiteralXn => "-"
196+
case _ => "-#+ 0,(<"
197+
}
138198

139199
def hasFlag(f: Char) = flags.contains(f)
140200
def hasAnyFlag(fs: String) = fs.exists(hasFlag)
@@ -146,6 +206,7 @@ abstract class FormatChecker(using reporter: InterpolationReporter):
146206
def errorAt(g: SpecGroup, i: Int = 0)(msg: String) = reporter.partError(msg, argi, descriptor.offset(g, i))
147207
def warningAt(g: SpecGroup, i: Int = 0)(msg: String) = reporter.partWarning(msg, argi, descriptor.offset(g, i))
148208

209+
// various assertions
149210
def noFlags = flags.isEmpty or errorAt(Flags)("flags not allowed")
150211
def noWidth = width.isEmpty or errorAt(Width)("width not allowed")
151212
def noPrecision = precision.isEmpty or errorAt(Precision)("precision not allowed")
@@ -162,84 +223,25 @@ abstract class FormatChecker(using reporter: InterpolationReporter):
162223
okRange || hasFlag('<') or errorAt(Index)("Argument index out of range")
163224
object Conversion:
164225
def apply(m: Match, i: Int): Conversion =
165-
def badCC(msg: String) = ErrorXn(m, i).tap(error => error.errorAt(if (error.op.isEmpty) Spec else CC)(msg))
166-
def cv(cc: Char) = cc match
167-
case 's' | 'S' => StringXn(m, i)
168-
case 'h' | 'H' => HashXn(m, i)
169-
case 'b' | 'B' => BooleanXn(m, i)
170-
case 'c' | 'C' => CharacterXn(m, i)
226+
def kindOf(cc: Char) = cc match
227+
case 's' | 'S' => StringXn
228+
case 'h' | 'H' => HashXn
229+
case 'b' | 'B' => BooleanXn
230+
case 'c' | 'C' => CharacterXn
171231
case 'd' | 'o' |
172-
'x' | 'X' => IntegralXn(m, i)
232+
'x' | 'X' => IntegralXn
173233
case 'e' | 'E' |
174234
'f' |
175235
'g' | 'G' |
176-
'a' | 'A' => FloatingPointXn(m, i)
177-
case 't' | 'T' => DateTimeXn(m, i)
178-
case '%' | 'n' => LiteralXn(m, i)
179-
case _ => badCC(s"illegal conversion character '$cc'")
180-
end cv
236+
'a' | 'A' => FloatingPointXn
237+
case 't' | 'T' => DateTimeXn
238+
case '%' | 'n' => LiteralXn
239+
case _ => ErrorXn
240+
end kindOf
181241
m.group(CC) match
182-
case Some(cc) => cv(cc(0)).tap(_.verify)
183-
case None => badCC(s"Missing conversion operator in '${m.matched}'; $literalHelp")
242+
case Some(cc) => new Conversion(m, i, kindOf(cc(0))).tap(_.verify)
243+
case None => new Conversion(m, i, ErrorXn).tap(_.errorAt(Spec)(s"Missing conversion operator in '${m.matched}'; $literalHelp"))
184244
end apply
185245
val literalHelp = "use %% for literal %, %n for newline"
246+
private val FakeNullTag: ClassTag[?] = null
186247
end Conversion
187-
abstract class GeneralXn extends Conversion
188-
// s | S
189-
class StringXn(val descriptor: Match, val argi: Int) extends GeneralXn:
190-
val acceptableVariants =
191-
if hasFlag('#') then classTag[Formattable] :: Nil
192-
else classTag[Any] :: Nil
193-
override protected def okFlags = "-#<"
194-
// b | B
195-
class BooleanXn(val descriptor: Match, val argi: Int) extends GeneralXn:
196-
val FakeNullTag: ClassTag[?] = null
197-
val acceptableVariants = classTag[Boolean] :: FakeNullTag :: Nil
198-
override def accepts(arg: ClassTag[?]): Boolean =
199-
arg == classTag[Boolean] orElse warningAt(CC)("Boolean format is null test for non-Boolean")
200-
override protected def okFlags = "-<"
201-
// h | H
202-
class HashXn(val descriptor: Match, val argi: Int) extends GeneralXn:
203-
val acceptableVariants = classTag[Any] :: Nil
204-
override protected def okFlags = "-<"
205-
// %% | %n
206-
class LiteralXn(val descriptor: Match, val argi: Int) extends Conversion:
207-
override def isLiteral = true
208-
override def verify = op match
209-
case "%" => super.verify && noPrecision and width.foreach(_ => warningAt(Width)("width ignored on literal"))
210-
case "n" => noFlags && noWidth && noPrecision
211-
override protected val okFlags = "-"
212-
override def acceptableVariants = Nil
213-
class CharacterXn(val descriptor: Match, val argi: Int) extends Conversion:
214-
override def verify = super.verify && noPrecision && only_-("c conversion")
215-
val acceptableVariants = classTag[Char] :: classTag[Byte] :: classTag[Short] :: classTag[Int] :: Nil
216-
class IntegralXn(val descriptor: Match, val argi: Int) extends Conversion:
217-
override def verify =
218-
def d_# = cc == 'd' && hasFlag('#') and badFlag('#', "# not allowed for d conversion")
219-
def x_comma = cc != 'd' && hasFlag(',') and badFlag(',', "',' only allowed for d conversion of integral types")
220-
super.verify && noPrecision && !d_# && !x_comma
221-
val acceptableVariants = classTag[Int] :: classTag[Long] :: classTag[Byte] :: classTag[Short] :: classTag[BigInt] :: Nil
222-
override def accepts(arg: ClassTag[?]): Boolean =
223-
arg == classTag[BigInt] || {
224-
cc match
225-
case 'o' | 'x' | 'X' if hasAnyFlag("+ (") => "+ (".filter(hasFlag).foreach(bad => badFlag(bad, s"only use '$bad' for BigInt conversions to o, x, X")) ; false
226-
case _ => true
227-
}
228-
class FloatingPointXn(val descriptor: Match, val argi: Int) extends Conversion:
229-
override def verify = super.verify && (cc match {
230-
case 'a' | 'A' =>
231-
val badFlags = ",(".filter(hasFlag)
232-
noPrecision && badFlags.isEmpty or badFlags.foreach(badf => badFlag(badf, s"'$badf' not allowed for a, A"))
233-
case _ => true
234-
})
235-
val acceptableVariants = classTag[Double] :: classTag[Float] :: classTag[BigDecimal] :: Nil
236-
class DateTimeXn(val descriptor: Match, val argi: Int) extends Conversion:
237-
override val cc: Char = if op.length > 1 then op(1) else '?'
238-
def hasCC = op.length == 2 or errorAt(CC)("Date/time conversion must have two characters")
239-
def goodCC = "HIklMSLNpzZsQBbhAaCYyjmdeRTrDFc".contains(cc) or errorAt(CC, 1)(s"'$cc' doesn't seem to be a date or time conversion")
240-
override def verify = super.verify && hasCC && goodCC && noPrecision && only_-("date/time conversions")
241-
val acceptableVariants = classTag[Long] :: classTag[Calendar] :: classTag[Date] :: Nil
242-
class ErrorXn(val descriptor: Match, val argi: Int) extends Conversion:
243-
override def isError = true
244-
override def verify = false
245-
override def acceptableVariants = Nil

compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,10 @@ class StringInterpolatorOpt extends MiniPhase:
112112
case nme.raw_ => sym eq defn.StringContext_raw
113113
case nme.f => sym eq defn.StringContext_f
114114
case _ => false
115+
// Perform format checking and normalization, then make it StringOps(fmt).format(args1) with tweaked args
115116
def transformF(fun: Tree, args: Tree): Tree =
116-
val (parts1, args1) = FormatInterpolatorTransform.checked(fun, args)
117-
resolveConstructor(defn.StringOps.typeRef, List(parts1))
117+
val (fmt, args1) = FormatInterpolatorTransform.checked(fun, args)
118+
resolveConstructor(defn.StringOps.typeRef, List(fmt))
118119
.select(nme.format)
119120
.appliedTo(args1)
120121
// Starting with Scala 2.13, s and raw are macros in the standard
@@ -135,6 +136,7 @@ class StringInterpolatorOpt extends MiniPhase:
135136
.appliedToTermArgs(List(process, args, parts))
136137
}
137138
end transformS
139+
// begin transformApply
138140
if isInterpolatedMethod then
139141
(tree: @unchecked) match
140142
case StringContextIntrinsic(strs: List[Literal], elems: List[Tree]) =>

0 commit comments

Comments
 (0)