From ba00a5b9344275b64935cf18191201cc54d59cdb Mon Sep 17 00:00:00 2001 From: Miguel Garcia Date: Mon, 16 Jan 2012 15:48:02 +0100 Subject: bringing down -Yinline time by 33% After inlining one ore more callsites, Inliner runs anew a type-flow analysis for the same method, ie. the previous solution is discarded although it's a subset of that needed after the inlining(s). Background in: http://lamp.epfl.ch/~magarcia/ScalaCompilerCornerReloaded/2011Q4/Inliner.pdf Instead, we invalidate only the fragment of the CFG affected by inlining (the basic block where inlining occurred and newly added basic blocks) and let the iterative dataflow analysis start from there. It reaches the fixpoint faster. The inlining decisions thus made are exactly the same as with the slower version, see -Ylog:inliner, with a focus on lines starting with "[log inliner] Inlining" review by @VladUreche @dragos @paulp --- .../backend/icode/analysis/TypeFlowAnalysis.scala | 44 ++++++++++++++ .../scala/tools/nsc/backend/opt/Inliners.scala | 70 ++++++++++++++-------- 2 files changed, 88 insertions(+), 26 deletions(-) (limited to 'src/compiler') diff --git a/src/compiler/scala/tools/nsc/backend/icode/analysis/TypeFlowAnalysis.scala b/src/compiler/scala/tools/nsc/backend/icode/analysis/TypeFlowAnalysis.scala index 937b0bdc3d..6421d6c8ef 100644 --- a/src/compiler/scala/tools/nsc/backend/icode/analysis/TypeFlowAnalysis.scala +++ b/src/compiler/scala/tools/nsc/backend/icode/analysis/TypeFlowAnalysis.scala @@ -619,6 +619,50 @@ abstract class TypeFlowAnalysis { } } + class MTFAGrowable extends MethodTFA { + + import icodes._ + + /** discards what must be discarded, blanks what needs to be blanked out, and keeps the rest. */ + def reinit(m: icodes.IMethod, staleOut: List[BasicBlock], inlined: collection.Set[BasicBlock], staleIn: collection.Set[BasicBlock]) { + if (this.method == null || this.method.symbol != m.symbol) { + init(m) + return + } else if(staleOut.isEmpty && inlined.isEmpty && staleIn.isEmpty) { + // this promotes invoking reinit if in doubt, no performance degradation will ensue! + return; + } + + reinit { + // asserts conveying an idea what CFG shapes arrive here. + // staleIn foreach (p => assert( !in.isDefinedAt(p), p)) + // staleIn foreach (p => assert(!out.isDefinedAt(p), p)) + // inlined foreach (p => assert( !in.isDefinedAt(p), p)) + // inlined foreach (p => assert(!out.isDefinedAt(p), p)) + // inlined foreach (p => assert(!p.successors.isEmpty || p.lastInstruction.isInstanceOf[icodes.opcodes.THROW], p)) + // staleOut foreach (p => assert( in.isDefinedAt(p), p)) + + // never rewrite in(m.startBlock) + staleOut foreach { b => + if(!inlined.contains(b)) { worklist += b } + out(b) = typeFlowLattice.bottom + } + // nothing else is added to the worklist, bb's reachable via succs will be tfa'ed + blankOut(inlined) + blankOut(staleIn) + // no need to add startBlocks from m.exh + } + } + + private def blankOut(blocks: collection.Set[BasicBlock]) { + blocks foreach { b => + in(b) = typeFlowLattice.bottom + out(b) = typeFlowLattice.bottom + } + } + + } + class Timer { var millis = 0L diff --git a/src/compiler/scala/tools/nsc/backend/opt/Inliners.scala b/src/compiler/scala/tools/nsc/backend/opt/Inliners.scala index 88bbb16e34..3e8ef3f611 100644 --- a/src/compiler/scala/tools/nsc/backend/opt/Inliners.scala +++ b/src/compiler/scala/tools/nsc/backend/opt/Inliners.scala @@ -102,11 +102,21 @@ abstract class Inliners extends SubComponent { debuglog("Analyzing " + cls) this.currentIClazz = cls - cls.methods filterNot (_.symbol.isConstructor) foreach analyzeMethod + val ms = cls.methods filterNot { _.symbol.isConstructor } + ms foreach { im => + if(hasInline(im.symbol)) { + log("Not inlining into " + im.symbol.originalName.decode + " because it is marked @inline.") + } else if(im.hasCode) { + analyzeMethod(im) + } + } } - val tfa = new analysis.MethodTFA() + val tfa = new analysis.MTFAGrowable() tfa.stat = global.opt.printStats + val staleOut = new mutable.ListBuffer[BasicBlock] + val splicedBlocks = mutable.Set.empty[BasicBlock] + val staleIn = mutable.Set.empty[BasicBlock] // how many times have we already inlined this method here? private val inlinedMethodCount = perRunCaches.newMap[Symbol, Int]() withDefaultValue 0 @@ -208,34 +218,35 @@ abstract class Inliners extends SubComponent { import scala.util.control.Breaks._ do { retry = false - if (caller.inline) { - log("Not inlining into " + caller.sym.originalName.decode + " because it is marked @inline.") - } - else if (caller.m.hasCode) { - log("Analyzing " + m + " count " + count + " with " + caller.length + " blocks") - tfa init m - tfa.run - caller.m.linearizedBlocks() foreach { bb => - info = tfa in bb - - breakable { - for (i <- bb) { - i match { - // Dynamic == normal invocations - // Static(true) == calls to private members - case CALL_METHOD(msym, Dynamic | Static(true)) if !msym.isConstructor => - if (analyzeInc(msym, i, bb)) - break - case _ => () - } - info = tfa.interpret(info, i) + log("Analyzing " + m + " count " + count + " with " + caller.length + " blocks") + tfa.reinit(m, staleOut.toList, splicedBlocks, staleIn) + tfa.run + staleOut.clear() + splicedBlocks.clear() + staleIn.clear() + + caller.m.linearizedBlocks() foreach { bb => + info = tfa in bb + + breakable { + for (i <- bb) { + i match { + // Dynamic == normal invocations + // Static(true) == calls to private members + case CALL_METHOD(msym, Dynamic | Static(true)) if !msym.isConstructor => + if (analyzeInc(msym, i, bb)) { + break + } + case _ => () } + info = tfa.interpret(info, i) } } - if (tfa.stat) - log(m.symbol.fullName + " iterations: " + tfa.iterations + " (size: " + caller.length + ")") } + + if (tfa.stat) + log(m.symbol.fullName + " iterations: " + tfa.iterations + " (size: " + caller.length + ")") } while (retry && count < MAX_INLINE_RETRY) @@ -343,6 +354,9 @@ abstract class Inliners extends SubComponent { * The instruction must be a CALL_METHOD. */ def doInline(block: BasicBlock, instr: Instruction) { + + staleOut += block + val targetPos = instr.pos log("Inlining " + inc.m + " in " + caller.m + " at pos: " + posToStr(targetPos)) @@ -478,7 +492,8 @@ abstract class Inliners extends SubComponent { block.close // duplicate the other blocks in the callee - inc.m.linearizedBlocks() foreach { bb => + val calleeLin = inc.m.linearizedBlocks() + calleeLin foreach { bb => var info = a in bb def emitInlined(i: Instruction) = inlinedBlock(bb).emit(i, targetPos) def emitDrops(toDrop: Int) = info.stack.types drop toDrop foreach (t => emitInlined(DROP(t))) @@ -503,6 +518,9 @@ abstract class Inliners extends SubComponent { afterBlock emit instrAfter afterBlock.close + staleIn += afterBlock + splicedBlocks ++= (calleeLin map inlinedBlock) + // add exception handlers of the callee caller addHandlers (inc.handlers map translateExh) assert(pending.isEmpty, "Pending NEW elements: " + pending) -- cgit v1.2.3 From 6ea9f72ffd0e1b606cb5ad4670b4db8330fa29b9 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 16 Jan 2012 17:49:12 +0100 Subject: A string interpolation implementation of SIP-11. This is the complete implementation of SIP-11, in its version of 15-Jan-2012. For now, the interpolations are enabled only under the -Xexperimental flag. --- src/compiler/scala/reflect/internal/StdNames.scala | 2 +- .../scala/tools/nsc/ast/parser/Parsers.scala | 63 +++--- .../scala/tools/nsc/ast/parser/Scanners.scala | 245 +++++++++++++-------- .../scala/tools/nsc/ast/parser/Tokens.scala | 10 +- src/library/scala/StringContext.scala | 191 ++++++++++++++++ test/files/run/interpolation.check | 20 ++ test/files/run/interpolation.flags | 1 + test/files/run/interpolation.scala | 24 ++ test/files/run/stringInterpolation.check | 2 - test/files/run/stringInterpolation.flags | 1 - test/files/run/stringInterpolation.scala | 7 - 11 files changed, 428 insertions(+), 138 deletions(-) create mode 100644 src/library/scala/StringContext.scala create mode 100644 test/files/run/interpolation.check create mode 100644 test/files/run/interpolation.flags create mode 100644 test/files/run/interpolation.scala delete mode 100644 test/files/run/stringInterpolation.check delete mode 100644 test/files/run/stringInterpolation.flags delete mode 100644 test/files/run/stringInterpolation.scala (limited to 'src/compiler') diff --git a/src/compiler/scala/reflect/internal/StdNames.scala b/src/compiler/scala/reflect/internal/StdNames.scala index 2871ba59f6..507621ea42 100644 --- a/src/compiler/scala/reflect/internal/StdNames.scala +++ b/src/compiler/scala/reflect/internal/StdNames.scala @@ -271,6 +271,7 @@ trait StdNames extends NameManglers { self: SymbolTable => // Compiler utilized names // val productElementName: NameType = "productElementName" val Ident: NameType = "Ident" + val StringContext: NameType = "StringContext" val TYPE_ : NameType = "TYPE" val TypeTree: NameType = "TypeTree" val UNIT : NameType = "UNIT" @@ -320,7 +321,6 @@ trait StdNames extends NameManglers { self: SymbolTable => val find_ : NameType = "find" val flatMap: NameType = "flatMap" val foreach: NameType = "foreach" - val formatted: NameType = "formatted" val freeValue : NameType = "freeValue" val genericArrayOps: NameType = "genericArrayOps" val get: NameType = "get" diff --git a/src/compiler/scala/tools/nsc/ast/parser/Parsers.scala b/src/compiler/scala/tools/nsc/ast/parser/Parsers.scala index 580b2a16eb..ce41bc456e 100644 --- a/src/compiler/scala/tools/nsc/ast/parser/Parsers.scala +++ b/src/compiler/scala/tools/nsc/ast/parser/Parsers.scala @@ -617,7 +617,7 @@ self => def isLiteralToken(token: Int) = token match { case CHARLIT | INTLIT | LONGLIT | FLOATLIT | DOUBLELIT | - STRINGLIT | STRINGPART | SYMBOLLIT | TRUE | FALSE | NULL => true + STRINGLIT | INTERPOLATIONID | SYMBOLLIT | TRUE | FALSE | NULL => true case _ => false } def isLiteral = isLiteralToken(in.token) @@ -1103,7 +1103,7 @@ self => * }}} * @note The returned tree does not yet have a position */ - def literal(isNegated: Boolean): Tree = { + def literal(isNegated: Boolean = false): Tree = { def finish(value: Any): Tree = { val t = Literal(Constant(value)) in.nextToken() @@ -1111,19 +1111,19 @@ self => } if (in.token == SYMBOLLIT) Apply(scalaDot(nme.Symbol), List(finish(in.strVal))) - else if (in.token == STRINGPART) + else if (in.token == INTERPOLATIONID) interpolatedString() else finish(in.token match { - case CHARLIT => in.charVal - case INTLIT => in.intVal(isNegated).toInt - case LONGLIT => in.intVal(isNegated) - case FLOATLIT => in.floatVal(isNegated).toFloat - case DOUBLELIT => in.floatVal(isNegated) - case STRINGLIT => in.strVal.intern() - case TRUE => true - case FALSE => false - case NULL => null - case _ => + case CHARLIT => in.charVal + case INTLIT => in.intVal(isNegated).toInt + case LONGLIT => in.intVal(isNegated) + case FLOATLIT => in.floatVal(isNegated).toFloat + case DOUBLELIT => in.floatVal(isNegated) + case STRINGLIT | STRINGPART => in.strVal.intern() + case TRUE => true + case FALSE => false + case NULL => null + case _ => syntaxErrorOrIncomplete("illegal literal", true) null }) @@ -1138,16 +1138,27 @@ self => } } - private def interpolatedString(): Tree = { - var t = atPos(o2p(in.offset))(New(TypeTree(definitions.StringBuilderClass.tpe), List(List()))) + private def interpolatedString(): Tree = atPos(in.offset) { + val start = in.offset + val interpolator = in.name + + val partsBuf = new ListBuffer[Tree] + val exprBuf = new ListBuffer[Tree] + in.nextToken() while (in.token == STRINGPART) { - t = stringOp(t, nme.append) - var e = expr() - if (in.token == STRINGFMT) e = stringOp(e, nme.formatted) - t = atPos(t.pos.startOrPoint)(Apply(Select(t, nme.append), List(e))) + partsBuf += literal() + exprBuf += { + if (in.token == IDENTIFIER) atPos(in.offset)(Ident(ident())) + else expr() + } } - if (in.token == STRINGLIT) t = stringOp(t, nme.append) - atPos(t.pos)(Select(t, nme.toString_)) + if (in.token == STRINGLIT) partsBuf += literal() + + val t1 = atPos(o2p(start)) { Ident(nme.StringContext) } + val t2 = atPos(start) { Apply(t1, partsBuf.toList) } + t2 setPos t2.pos.makeTransparent + val t3 = Select(t2, interpolator) setPos t2.pos + atPos(start) { Apply(t3, exprBuf.toList) } } /* ------------- NEW LINES ------------------------------------------------- */ @@ -1469,7 +1480,7 @@ self => atPos(in.offset) { val name = nme.toUnaryName(rawIdent()) // val name = nme.toUnaryName(ident()) // val name: Name = "unary_" + ident() - if (name == nme.UNARY_- && isNumericLit) simpleExprRest(atPos(in.offset)(literal(true)), true) + if (name == nme.UNARY_- && isNumericLit) simpleExprRest(atPos(in.offset)(literal(isNegated = true)), true) else Select(stripParens(simpleExpr()), name) } } @@ -1493,7 +1504,7 @@ self => def simpleExpr(): Tree = { var canApply = true val t = - if (isLiteral) atPos(in.offset)(literal(false)) + if (isLiteral) atPos(in.offset)(literal()) else in.token match { case XMLSTART => xmlLiteral() @@ -1827,7 +1838,7 @@ self => case INTLIT | LONGLIT | FLOATLIT | DOUBLELIT => t match { case Ident(nme.MINUS) => - return atPos(start) { literal(true) } + return atPos(start) { literal(isNegated = true) } case _ => } case _ => @@ -1844,8 +1855,8 @@ self => in.nextToken() atPos(start, start) { Ident(nme.WILDCARD) } case CHARLIT | INTLIT | LONGLIT | FLOATLIT | DOUBLELIT | - STRINGLIT | STRINGPART | SYMBOLLIT | TRUE | FALSE | NULL => - atPos(start) { literal(false) } + STRINGLIT | INTERPOLATIONID | SYMBOLLIT | TRUE | FALSE | NULL => + atPos(start) { literal() } case LPAREN => atPos(start)(makeParens(noSeq.patterns())) case XMLSTART => diff --git a/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala b/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala index a25b3afbc6..a2a577a7ab 100644 --- a/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala +++ b/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala @@ -161,12 +161,25 @@ trait Scanners extends ScannersCommon { * RBRACKET if region starts with '[' * RBRACE if region starts with '{' * ARROW if region starts with `case' - * STRINGFMT if region is a string interpolation expression starting with '\{' + * STRINGLIT if region is a string interpolation expression starting with '${' + * (the STRINGLIT appears twice in succession on the stack iff the + * expression is a multiline string literal). */ var sepRegions: List[Int] = List() // Get next token ------------------------------------------------------------ + /** Are we directly in a string interpolation expression? + */ + @inline private def inStringInterpolation = + sepRegions.nonEmpty && sepRegions.head == STRINGLIT + + /** Are we directly in a multiline string interpolation expression? + * @pre: inStringInterpolation + */ + @inline private def inMultiLineInterpolation = + sepRegions.tail.nonEmpty && sepRegions.tail.head == STRINGPART + /** read next token and return last offset */ def skipToken(): Offset = { @@ -189,29 +202,31 @@ trait Scanners extends ScannersCommon { sepRegions = RBRACE :: sepRegions case CASE => sepRegions = ARROW :: sepRegions - case STRINGPART => - sepRegions = STRINGFMT :: sepRegions case RBRACE => - sepRegions = sepRegions dropWhile (_ != RBRACE) + while (!sepRegions.isEmpty && sepRegions.head != RBRACE) + sepRegions = sepRegions.tail if (!sepRegions.isEmpty) sepRegions = sepRegions.tail - case RBRACKET | RPAREN | ARROW | STRINGFMT => + docBuffer = null + case RBRACKET | RPAREN => if (!sepRegions.isEmpty && sepRegions.head == lastToken) sepRegions = sepRegions.tail - case _ => - } - (lastToken: @switch) match { - case RBRACE | RBRACKET | RPAREN => docBuffer = null + case ARROW => + if (!sepRegions.isEmpty && sepRegions.head == lastToken) + sepRegions = sepRegions.tail + case STRINGLIT => + if (inStringInterpolation) + sepRegions = sepRegions.tail case _ => } - + // Read a token or copy it from `next` tokenData if (next.token == EMPTY) { lastOffset = charOffset - 1 - if(lastOffset > 0 && buf(lastOffset) == '\n' && buf(lastOffset - 1) == '\r') { + if (lastOffset > 0 && buf(lastOffset) == '\n' && buf(lastOffset - 1) == '\r') { lastOffset -= 1 } - fetchToken() + if (inStringInterpolation) fetchStringPart() else fetchToken() } else { this copyFrom next next.token = EMPTY @@ -308,7 +323,9 @@ trait Scanners extends ScannersCommon { 'z' => putChar(ch) nextChar() - getIdentRest() // scala-mode: wrong indent for multi-line case blocks + getIdentRest() + if (ch == '"' && token == IDENTIFIER && settings.Xexperimental.value) + token = INTERPOLATIONID case '<' => // is XMLSTART? val last = if (charOffset >= 2) buf(charOffset - 2) else ' ' nextChar() @@ -360,18 +377,37 @@ trait Scanners extends ScannersCommon { case '`' => getBackquotedIdent() case '\"' => - nextChar() - if (ch == '\"') { - nextChar() + if (token == INTERPOLATIONID) { + nextRawChar() if (ch == '\"') { nextRawChar() - getMultiLineStringLit() + if (ch == '\"') { + nextRawChar() + getStringPart(multiLine = true) + sepRegions = STRINGLIT :: sepRegions // indicate string part + sepRegions = STRINGLIT :: sepRegions // once more to indicate multi line string part + } else { + token = STRINGLIT + strVal = "" + } } else { - token = STRINGLIT - strVal = "" + getStringPart(multiLine = false) + sepRegions = STRINGLIT :: sepRegions // indicate single line string part } } else { - getStringPart() + nextChar() + if (ch == '\"') { + nextChar() + if (ch == '\"') { + nextRawChar() + getRawStringLit() + } else { + token = STRINGLIT + strVal = "" + } + } else { + getStringLit() + } } case '\'' => nextChar() @@ -397,9 +433,7 @@ trait Scanners extends ScannersCommon { token = DOT } case ';' => - nextChar() - if (inStringInterpolation) getFormatString() - else token = SEMI + nextChar(); token = SEMI case ',' => nextChar(); token = COMMA case '(' => @@ -409,16 +443,7 @@ trait Scanners extends ScannersCommon { case ')' => nextChar(); token = RPAREN case '}' => - if (token == STRINGFMT) { - nextChar() - getStringPart() - } else if (inStringInterpolation) { - strVal = ""; - token = STRINGFMT - } else { - nextChar(); - token = RBRACE - } + nextChar(); token = RBRACE case '[' => nextChar(); token = LBRACKET case ']' => @@ -506,11 +531,6 @@ trait Scanners extends ScannersCommon { } } - /** Are we directly in a string interpolation expression? - */ - private def inStringInterpolation = - sepRegions.nonEmpty && sepRegions.head == STRINGFMT - /** Can token start a statement? */ def inFirstOfStat(token: Int) = token match { case EOF | CATCH | ELSE | EXTENDS | FINALLY | FORSOME | MATCH | WITH | YIELD | @@ -608,71 +628,110 @@ trait Scanners extends ScannersCommon { else finishNamed() } } + + +// Literals ----------------------------------------------------------------- - def getFormatString() = { - getLitChars('}', '"', ' ', '\t') - if (ch == '}') { - setStrVal() - if (strVal.length > 0) strVal = "%" + strVal - token = STRINGFMT - } else { - syntaxError("unclosed format string") - } - } - - def getStringPart() = { - while (ch != '"' && (ch != CR && ch != LF && ch != SU || isUnicodeEscape) && maybeGetLitChar()) {} + private def getStringLit() = { + getLitChars('"') if (ch == '"') { setStrVal() nextChar() token = STRINGLIT - } else if (ch == '{' && settings.Xexperimental.value) { - setStrVal() - nextChar() - token = STRINGPART - } else { - syntaxError("unclosed string literal") - } + } else syntaxError("unclosed string literal") } - private def getMultiLineStringLit() { + private def getRawStringLit(): Unit = { if (ch == '\"') { nextRawChar() - if (ch == '\"') { + if (isTripleQuote()) { + setStrVal() + token = STRINGLIT + } else + getRawStringLit() + } else if (ch == SU) { + incompleteInputError("unclosed multi-line string literal") + } else { + putChar(ch) + nextRawChar() + getRawStringLit() + } + } + + @annotation.tailrec private def getStringPart(multiLine: Boolean): Unit = { + def finishStringPart() = { + setStrVal() + token = STRINGPART + next.lastOffset = charOffset - 1 + next.offset = charOffset - 1 + } + if (ch == '"') { + nextRawChar() + if (!multiLine || isTripleQuote()) { + setStrVal() + token = STRINGLIT + } else + getStringPart(multiLine) + } else if (ch == '$') { + nextRawChar() + if (ch == '$') { + putChar(ch) nextRawChar() - if (ch == '\"') { - nextChar() - while (ch == '\"') { - putChar('\"') - nextChar() - } - token = STRINGLIT - setStrVal() - } else { - putChar('\"') - putChar('\"') - getMultiLineStringLit() - } + getStringPart(multiLine) + } else if (ch == '{') { + finishStringPart() + nextRawChar() + next.token = LBRACE + } else if (Character.isUnicodeIdentifierStart(ch)) { + finishStringPart() + do { + putChar(ch) + nextRawChar() + } while (Character.isUnicodeIdentifierPart(ch)) + next.token = IDENTIFIER + next.name = newTermName(cbuf.toString) + cbuf.clear() } else { - putChar('\"') - getMultiLineStringLit() + syntaxError("invalid string interpolation") } - } else if (ch == SU) { - incompleteInputError("unclosed multi-line string literal") + } else if ((ch == CR || ch == LF || ch == SU) && !isUnicodeEscape) { + syntaxError("unclosed string literal") } else { putChar(ch) nextRawChar() - getMultiLineStringLit() + getStringPart(multiLine) } } + + private def fetchStringPart() = { + offset = charOffset - 1 + getStringPart(multiLine = inMultiLineInterpolation) + } + + private def isTripleQuote(): Boolean = + if (ch == '"') { + nextRawChar() + if (ch == '"') { + nextChar() + while (ch == '"') { + putChar('"') + nextChar() + } + true + } else { + putChar('"') + putChar('"') + false + } + } else { + putChar('"') + false + } -// Literals ----------------------------------------------------------------- - - /** read next character in character or string literal: - * if character sequence is a \{ escape, do not copy it into the string and return false. - * otherwise return true. + /** copy current character into cbuf, interpreting any escape sequences, + * and advance to next character. */ - protected def maybeGetLitChar(): Boolean = { + protected def getLitChar(): Unit = if (ch == '\\') { nextChar() if ('0' <= ch && ch <= '7') { @@ -698,7 +757,6 @@ trait Scanners extends ScannersCommon { case '\"' => putChar('\"') case '\'' => putChar('\'') case '\\' => putChar('\\') - case '{' => return false case _ => invalidEscape() } nextChar() @@ -707,22 +765,16 @@ trait Scanners extends ScannersCommon { putChar(ch) nextChar() } - true - } protected def invalidEscape(): Unit = { syntaxError(charOffset - 1, "invalid escape character") putChar(ch) } - protected def getLitChar(): Unit = - if (!maybeGetLitChar()) invalidEscape() - - private def getLitChars(delimiters: Char*) { - while (!(delimiters contains ch) && (ch != CR && ch != LF && ch != SU || isUnicodeEscape)) { + private def getLitChars(delimiter: Char) = + while (ch != delimiter && (ch != CR && ch != LF && ch != SU || isUnicodeEscape)) { getLitChar() } - } /** read fractional part and exponent of floating point number * if one is present. @@ -971,8 +1023,8 @@ trait Scanners extends ScannersCommon { "string(" + strVal + ")" case STRINGPART => "stringpart(" + strVal + ")" - case STRINGFMT => - "stringfmt(" + strVal + ")" + case INTERPOLATIONID => + "interpolationid(" + name + ")" case SEMI => ";" case NEWLINE => @@ -1088,8 +1140,7 @@ trait Scanners extends ScannersCommon { case LONGLIT => "long literal" case FLOATLIT => "float literal" case DOUBLELIT => "double literal" - case STRINGLIT | STRINGPART => "string literal" - case STRINGFMT => "format string" + case STRINGLIT | STRINGPART | INTERPOLATIONID => "string literal" case SYMBOLLIT => "symbol literal" case LPAREN => "'('" case RPAREN => "')'" diff --git a/src/compiler/scala/tools/nsc/ast/parser/Tokens.scala b/src/compiler/scala/tools/nsc/ast/parser/Tokens.scala index 07970bb36e..091f333c27 100644 --- a/src/compiler/scala/tools/nsc/ast/parser/Tokens.scala +++ b/src/compiler/scala/tools/nsc/ast/parser/Tokens.scala @@ -45,18 +45,20 @@ abstract class Tokens { } object Tokens extends Tokens { - final val STRINGPART = 7 + final val STRINGPART = 7 // a part of an interpolated string final val SYMBOLLIT = 8 - final val STRINGFMT = 9 + final val INTERPOLATIONID = 9 // the lead identifier of an interpolated string + def isLiteral(code: Int) = - code >= CHARLIT && code <= SYMBOLLIT + code >= CHARLIT && code <= INTERPOLATIONID + /** identifiers */ final val IDENTIFIER = 10 final val BACKQUOTED_IDENT = 11 def isIdentifier(code: Int) = code >= IDENTIFIER && code <= BACKQUOTED_IDENT - + @switch def canBeginExpression(code: Int) = code match { case IDENTIFIER|BACKQUOTED_IDENT|USCORE => true case LBRACE|LPAREN|LBRACKET|COMMENT => true diff --git a/src/library/scala/StringContext.scala b/src/library/scala/StringContext.scala new file mode 100644 index 0000000000..1f0b4c766e --- /dev/null +++ b/src/library/scala/StringContext.scala @@ -0,0 +1,191 @@ +/* __ *\ +** ________ ___ / / ___ Scala API ** +** / __/ __// _ | / / / _ | (c) 2002-2012, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** +** /____/\___/_/ |_/____/_/ | | ** +** |/ ** +\* */ + +package scala + +import collection.mutable.ArrayBuffer + +/** A class to support string interpolation. + * This class supports string interpolation as outlined in Scala SIP-11. + * It needs to be fully documented once the SIP is accepted. + * + * @param parts The parts that make up the interpolated string, + * without the expressions that get inserted by interpolation. + */ +case class StringContext(parts: String*) { + + import StringContext._ + + /** Checks that the given arguments `args` number one less than the number + * of `parts` supplied to the enclosing `StringContext`. + * @param `args` The arguments to be checked. + * @throws An `IllegalArgumentException` if this is not the case. + */ + def checkLengths(args: Any*): Unit = + if (parts.length != args.length + 1) + throw new IllegalArgumentException("wrong number of arguments for interpolated string") + + + /** The simple string interpolator. + * + * It inserts its arguments between corresponding parts of the string context. + * It also treats standard escape sequences as defined in the Scala specification. + * @param `args` The arguments to be inserted into the resulting string. + * @throws An `IllegalArgumentException` + * if the number of `parts` in the enclosing `StringContext` does not exceed + * the number of arguments `arg` by exactly 1. + * @throws A `StringContext.InvalidEscapeException` if if a `parts` string contains a backslash (`\`) character + * that does not start a valid escape sequence. + */ + def s(args: Any*) = { + checkLengths(args) + val pi = parts.iterator + val ai = args.iterator + val bldr = new java.lang.StringBuilder(treatEscapes(pi.next)) + while (ai.hasNext) { + bldr append ai.next + bldr append treatEscapes(pi.next) + } + bldr.toString + } + + /** The formatted string interpolator. + * + * It inserts its arguments between corresponding parts of the string context. + * It also treats standard escape sequences as defined in the Scala specification. + * Finally, if an interpolated expression is followed by a `parts` string + * that starts with a formatting specifier, the expression is formatted according to that + * specifier. All specifiers allowed in Java format strings are handled, and in the same + * way they are treated in Java. + * + * @param `args` The arguments to be inserted into the resulting string. + * @throws An `IllegalArgumentException` + * if the number of `parts` in the enclosing `StringContext` does not exceed + * the number of arguments `arg` by exactly 1. + * @throws A `StringContext.InvalidEscapeException` if a `parts` string contains a backslash (`\`) character + * that does not start a valid escape sequence. + * + * Note: The `f` method works by assembling a format string from all the `parts` strings and using + * `java.lang.String.format` to format all arguments with that format string. The format string is + * obtained by concatenating all `parts` strings, and performing two transformations: + * + * 1. Let a _formatting position_ be a start of any `parts` string except the first one. + * If a formatting position does not refer to a `%` character (which is assumed to + * start a format specifier), then the string format specifier `%s` is inserted. + * + * 2. Any `%` characters not in formatting positions are left in the resulting + * string literally. This is achieved by replacing each such occurrence by a string + * format specifier `%s` and adding a corresponding argument string `"%"`. + */ + def f(args: Any*) = { + checkLengths(args) + val pi = parts.iterator + val ai = args.iterator + val bldr = new java.lang.StringBuilder + val args1 = new ArrayBuffer[Any] + def copyString(first: Boolean): Unit = { + val str = treatEscapes(pi.next) + var start = 0 + var idx = 0 + if (!first) { + if ((str charAt 0) != '%') + bldr append "%s" + idx = 1 + } + val len = str.length + while (idx < len) { + if (str(idx) == '%') { + bldr append (str substring (start, idx)) append "%s" + args1 += "%" + start = idx + 1 + } + idx += 1 + } + bldr append (str substring (start, idx)) + } + copyString(first = true) + while (pi.hasNext) { + args1 += ai.next + copyString(first = false) + } + bldr.toString format (args1: _*) + } +} + +object StringContext { + + /** An exception that is thrown if a string contains a backslash (`\`) character that + * that does not start a valid escape sequence. + * @param str The offending string + * @param idx The index of the offending backslash character in `str`. + */ + class InvalidEscapeException(str: String, idx: Int) + extends IllegalArgumentException("invalid escape character at index "+idx+" in \""+str+"\"") + + /** Expands standard Scala escape sequences in a string. + * Escape sequences are: + * control: `\b`, `\t`, `\n`, `\f`, `\r` + * escape: `\\`, `\"`, `\'` + * octal: `\d` `\dd` `\ddd` where `d` is an octal digit between `0` and `7`. + * + * @param A string that may contain escape sequences + * @return The string with all escape sequences expanded. + */ + def treatEscapes(str: String): String = { + lazy val bldr = new java.lang.StringBuilder + val len = str.length + var start = 0 + var cur = 0 + var idx = 0 + def output(ch: Char) = { + bldr append str substring (start, cur) + bldr append ch + start = idx + } + while (idx < len) { + cur = idx + if (str(idx) == '\\') { + idx += 1 + if ('0' <= str(idx) && str(idx) <= '7') { + val leadch = str(idx) + var oct = leadch - '0' + idx += 1 + if ('0' <= str(idx) && str(idx) <= '7') { + oct = oct * 8 + str(idx) - '0' + idx += 1 + if (leadch <= '3' && '0' <= str(idx) && str(idx) <= '7') { + oct = oct * 8 + str(idx) - '0' + idx += 1 + } + } + output(oct.toChar) + } else { + val ch = str(idx) + idx += 1 + output { + ch match { + case 'b' => '\b' + case 't' => '\t' + case 'n' => '\n' + case 'f' => '\f' + case 'r' => '\r' + case '\"' => '\"' + case '\'' => '\'' + case '\\' => '\\' + case _ => throw new InvalidEscapeException(str, cur) + } + } + } + } else { + idx += 1 + } + } + if (start == 0) str + else (bldr append str.substring(start, idx)).toString + } +} diff --git a/test/files/run/interpolation.check b/test/files/run/interpolation.check new file mode 100644 index 0000000000..4c34e4c8c8 --- /dev/null +++ b/test/files/run/interpolation.check @@ -0,0 +1,20 @@ +Bob is 1 years old +Bob is 1 years old +Bob will be 2 years old +Bob will be 2 years old +Bob is 12 years old +Bob is 12 years old +Bob will be 13 years old +Bob will be 13 years old +Bob is 123 years old +Bob is 123 years old +Bob will be 124 years old +Bob will be 124 years old +Best price: 10.0 +Best price: 10.00 +10.0% discount included +10.00% discount included +Best price: 13.345 +Best price: 13.35 +13.345% discount included +13.35% discount included diff --git a/test/files/run/interpolation.flags b/test/files/run/interpolation.flags new file mode 100644 index 0000000000..48fd867160 --- /dev/null +++ b/test/files/run/interpolation.flags @@ -0,0 +1 @@ +-Xexperimental diff --git a/test/files/run/interpolation.scala b/test/files/run/interpolation.scala new file mode 100644 index 0000000000..232a180bcd --- /dev/null +++ b/test/files/run/interpolation.scala @@ -0,0 +1,24 @@ +object Test extends App { + + def test1(n: Int) = { + println(s"Bob is $n years old") + println(f"Bob is $n%2d years old") + println(s"Bob will be ${n+1} years old") + println(f"Bob will be ${n+1}%2d years old") + } + + def test2(f: Float) = { + println(s"Best price: $f") + println(f"Best price: $f%.2f") + println(s"$f% discount included") + println(f"$f%3.2f% discount included") + } + + test1(1) + test1(12) + test1(123) + + test2(10.0f) + test2(13.345f) + +} diff --git a/test/files/run/stringInterpolation.check b/test/files/run/stringInterpolation.check deleted file mode 100644 index b5b63343a8..0000000000 --- a/test/files/run/stringInterpolation.check +++ /dev/null @@ -1,2 +0,0 @@ -1 plus 1 is 2 -We have a 1.10% chance of success diff --git a/test/files/run/stringInterpolation.flags b/test/files/run/stringInterpolation.flags deleted file mode 100644 index 48fd867160..0000000000 --- a/test/files/run/stringInterpolation.flags +++ /dev/null @@ -1 +0,0 @@ --Xexperimental diff --git a/test/files/run/stringInterpolation.scala b/test/files/run/stringInterpolation.scala deleted file mode 100644 index d88f5f6889..0000000000 --- a/test/files/run/stringInterpolation.scala +++ /dev/null @@ -1,7 +0,0 @@ -object Test { - def main(args : Array[String]) : Unit = { - println("\{1} plus \{1} is \{1 + 1}") - val x = 1.1 - println("We have a \{ x ;2.2f}% chance of success") - } -} -- cgit v1.2.3