diff --git a/src/dparse/ast.d b/src/dparse/ast.d index eba8d1a3..ebc24d36 100644 --- a/src/dparse/ast.d +++ b/src/dparse/ast.d @@ -16,10 +16,10 @@ module dparse.ast; import dparse.lexer; -import std.traits; import std.algorithm; import std.array; import std.string; +import std.traits; private immutable uint[TypeInfo] typeMap; @@ -73,6 +73,8 @@ shared static this() typeMap[typeid(TypeofExpression)] = 46; typeMap[typeid(UnaryExpression)] = 47; typeMap[typeid(XorExpression)] = 48; + typeMap[typeid(InterpolatedStringExpression)] = 49; + typeMap[typeid(InterpolatedStringText)] = 50; } /// Describes which syntax was used in a list of declarations in the containing AST node @@ -167,6 +169,18 @@ abstract class ASTVisitor case 46: visit(cast(TypeofExpression) n); break; case 47: visit(cast(UnaryExpression) n); break; case 48: visit(cast(XorExpression) n); break; + // skip 49, 50 (used for InterpolatedStringPart) + default: assert(false, __MODULE__ ~ " has a bug"); + } + } + + /// ditto + void dynamicDispatch(const InterpolatedStringPart n) + { + switch (typeMap.get(typeid(n), 0)) + { + case 49: visit(cast(InterpolatedStringExpression) n); break; + case 50: visit(cast(InterpolatedStringText) n); break; default: assert(false, __MODULE__ ~ " has a bug"); } } @@ -289,6 +303,9 @@ abstract class ASTVisitor /** */ void visit(const Initialize initialize) { initialize.accept(this); } /** */ void visit(const Initializer initializer) { initializer.accept(this); } /** */ void visit(const InterfaceDeclaration interfaceDeclaration) { interfaceDeclaration.accept(this); } + /** */ void visit(const InterpolatedString interpolatedString) { interpolatedString.accept(this); } + /** */ void visit(const InterpolatedStringExpression interpolatedStringExpression) { interpolatedStringExpression.accept(this); } + /** */ void visit(const InterpolatedStringText interpolatedStringText) { interpolatedStringText.accept(this); } /** */ void visit(const Invariant invariant_) { invariant_.accept(this); } /** */ void visit(const IsExpression isExpression) { isExpression.accept(this); } /** */ void visit(const KeyValuePair keyValuePair) { keyValuePair.accept(this); } @@ -426,7 +443,7 @@ template visitIfNotNull(fields ...) } } -mixin template OpEquals(bool print = false) +private mixin template OpEquals(extraFields...) { override bool opEquals(Object other) const { @@ -443,6 +460,9 @@ mixin template OpEquals(bool print = false) if (field != obj.tupleof[i]) return false; } + static foreach (field; extraFields) + if (mixin("this." ~ field ~ " != obj." ~ field)) + return false; return true; } return false; @@ -1402,8 +1422,8 @@ final class Declaration : BaseNode } } - private import std.variant:Algebraic; - private import std.typetuple:TypeTuple; + import std.typetuple : TypeTuple; + import std.variant : Algebraic; alias DeclarationTypes = TypeTuple!(AliasDeclaration, AliasAssign, AliasThisDeclaration, AnonymousEnumDeclaration, AttributeDeclaration, @@ -2318,6 +2338,87 @@ final class InterfaceDeclaration : BaseNode mixin OpEquals; } +/// +final class InterpolatedString : BaseNode +{ + override void accept(ASTVisitor visitor) const + { + mixin (visitIfNotNull!(parts)); + } + + /** */ InterpolatedStringPart[] parts; + + inout(Token) startQuote() inout pure nothrow @nogc @safe scope + { + return tokens.length ? tokens[0] : Token.init; + } + + inout(Token) endQuote() inout pure nothrow @nogc @safe scope + { + return tokens.length && tokens[$ - 1].type == tok!"istringLiteralEnd" + ? tokens[$ - 1] + : Token.init; + } + + /// '\0'/'c'/'w'/'d' for `i""`, `i""c`, `i""w` and `i""d` respectively. + char postfixType() inout pure nothrow @nogc @safe scope + { + auto end = endQuote.text; + auto endChar = end.length ? end[$ - 1] : ' '; + switch (endChar) + { + case 'c': + case 'w': + case 'd': + return endChar; + default: + return '\0'; + } + } + + mixin OpEquals!("startQuote.text", "postfixType"); +} + +/// AST nodes within an interpolated string +abstract class InterpolatedStringPart : BaseNode +{ +} + +/// Just plain text inside the interpolated string +final class InterpolatedStringText : InterpolatedStringPart +{ + override void accept(ASTVisitor visitor) const + { + } + + /// The token containing the plain text part in its `.text` property. + inout(Token) text() inout pure nothrow @nogc @safe scope + { + return tokens.length ? tokens[0] : Token.init; + } + + mixin OpEquals!("text.text"); +} + +/// A $(...) interpolation sequence +final class InterpolatedStringExpression : InterpolatedStringPart +{ + override void accept(ASTVisitor visitor) const + { + mixin (visitIfNotNull!(expression)); + } + + /** */ Expression expression; + + /// The dollar token. + inout(Token) dollar() inout pure nothrow @nogc @safe scope + { + return tokens.length ? tokens[0] : Token.init; + } + + mixin OpEquals; +} + /// final class Invariant : BaseNode { @@ -2798,7 +2899,7 @@ final class PrimaryExpression : ExpressionNode typeofExpression, typeidExpression, arrayLiteral, assocArrayLiteral, expression, dot, identifierOrTemplateInstance, isExpression, functionLiteralExpression,traitsExpression, mixinExpression, - importExpression, vector, arguments)); + importExpression, vector, arguments, interpolatedString)); } /** */ Token dot; /** */ Token primary; @@ -2818,6 +2919,7 @@ final class PrimaryExpression : ExpressionNode /** */ Type type; /** */ Token typeConstructor; /** */ Arguments arguments; + /** */ InterpolatedString interpolatedString; mixin OpEquals; } @@ -3356,9 +3458,10 @@ final class TemplateSingleArgument : BaseNode { override void accept(ASTVisitor visitor) const { - mixin (visitIfNotNull!(token)); + mixin (visitIfNotNull!(token, istring)); } /** */ Token token; + /** */ InterpolatedString istring; mixin OpEquals; } @@ -3965,7 +4068,7 @@ unittest //#365 : used to segfault unittest // issue #398: Support extern(C++, ) { import dparse.lexer : LexerConfig; - import dparse.parser : ParserConfig, parseModule; + import dparse.parser : parseModule, ParserConfig; import dparse.rollback_allocator : RollbackAllocator; RollbackAllocator ra; diff --git a/src/dparse/astprinter.d b/src/dparse/astprinter.d index ba17f6d6..aae7c57b 100644 --- a/src/dparse/astprinter.d +++ b/src/dparse/astprinter.d @@ -582,6 +582,28 @@ class XMLPrinter : ASTVisitor output.writeln(""); } + override void visit(const InterpolatedString interpolatedString) + { + output.writeln(""); + foreach (part; interpolatedString.parts) + dynamicDispatch(part); + output.writeln(""); + } + + override void visit(const InterpolatedStringText interpolatedStringText) + { + output.writeln("", xmlEscape(interpolatedStringText.text.text), ""); + } + + override void visit(const InterpolatedStringExpression interpolatedStringExpression) + { + visit(interpolatedStringExpression.expression); + } + override void visit(const Invariant invariant_) { output.writeln(""); diff --git a/src/dparse/formatter.d b/src/dparse/formatter.d index 9fbfa3de..77217439 100644 --- a/src/dparse/formatter.d +++ b/src/dparse/formatter.d @@ -12,8 +12,8 @@ version (unittest) { import dparse.parser; import dparse.rollback_allocator; - import std.array : Appender; import std.algorithm : canFind; + import std.array : Appender; } //debug = verbose; @@ -2013,6 +2013,29 @@ class Formatter(Sink) } } + void format(const InterpolatedString interpolatedString) + { + put(interpolatedString.startQuote.text); + foreach (part; interpolatedString.parts) + { + if (cast(InterpolatedStringText) part) format(cast(InterpolatedStringText) part); + else if (cast(InterpolatedStringExpression) part) format(cast(InterpolatedStringExpression) part); + } + put(interpolatedString.endQuote.text); + } + + void format(const InterpolatedStringText interpolatedStringText) + { + put(interpolatedStringText.text.text); + } + + void format(const InterpolatedStringExpression interpolatedStringExpression) + { + put("$("); + format(interpolatedStringExpression.expression); + put(")"); + } + void format(const Invariant invariant_, const Attribute[] attrs = null) { debug(verbose) writeln("Invariant"); @@ -2572,6 +2595,7 @@ class Formatter(Sink) Type type; Token typeConstructor; Arguments arguments; + InterpolatedString interpolatedString; **/ with(primaryExpression) @@ -2606,6 +2630,7 @@ class Formatter(Sink) else if (vector) format(vector); else if (type) format(type); else if (arguments) format(arguments); + else if (interpolatedString) format(interpolatedString); } } @@ -3258,7 +3283,10 @@ class Formatter(Sink) **/ put("!"); - format(templateSingleArgument.token); + if (templateSingleArgument.istring) + format(templateSingleArgument.istring); + else + format(templateSingleArgument.token); } void format(const TemplateThisParameter templateThisParameter) @@ -4367,4 +4395,16 @@ y, /// Documentation for y z /// Documentation for z }"); + testFormatNode!(VariableDeclaration)(`T x = i"hello";`); + testFormatNode!(VariableDeclaration)(`T x = i" hello ";`); + testFormatNode!(VariableDeclaration)(`T x = i" hello $name ";`); + testFormatNode!(VariableDeclaration)(`T x = i" hello $(name) ";`); + testFormatNode!(VariableDeclaration)(`T x = i" hello $( name ) ";`, `T x = i" hello $(name) ";`); + testFormatNode!(VariableDeclaration)(`auto a = iq{ "}" hi };`, `auto a = iq{ "}" hi };`); + testFormatNode!(VariableDeclaration)("T x = iq{\n};"); + testFormatNode!(AliasDeclaration)(`alias expr = AliasSeq!i"$(a) $(b)";`); + testFormatNode!(VariableDeclaration)("auto thing = i\"$(b) $(\"$\" ~ ')' ~ `\"`)\";"); + testFormatNode!(VariableDeclaration)("auto x = i` $(b) is $(b)!`;"); + testFormatNode!(VariableDeclaration)("auto x = iq{ $(b) is $(b)!};"); + testFormatNode!(VariableDeclaration)("auto x = iq{{$('$')}};"); } diff --git a/src/dparse/lexer.d b/src/dparse/lexer.d index 2cd30525..64713f9a 100644 --- a/src/dparse/lexer.d +++ b/src/dparse/lexer.d @@ -1,13 +1,13 @@ module dparse.lexer; -import std.typecons; -import std.typetuple; -import std.array; +import core.cpuid : sse42; import std.algorithm; -import std.range; +import std.array; import std.experimental.lexer; +import std.range; import std.traits; -import core.cpuid : sse42; +import std.typecons; +import std.typetuple; public import dparse.trivia; @@ -47,7 +47,8 @@ private immutable dynamicTokens = [ "whitespace", "doubleLiteral", "floatLiteral", "idoubleLiteral", "ifloatLiteral", "intLiteral", "longLiteral", "realLiteral", "irealLiteral", "uintLiteral", "ulongLiteral", "characterLiteral", - "dstringLiteral", "stringLiteral", "wstringLiteral" + "dstringLiteral", "stringLiteral", "wstringLiteral", "istringLiteralStart", + "istringLiteralText", "istringLiteralEnd" ]; private immutable pseudoTokenHandlers = [ @@ -68,6 +69,9 @@ private immutable pseudoTokenHandlers = [ "7", "lexDecimal", "8", "lexDecimal", "9", "lexDecimal", + "i\"", "lexInterpolatedString", + "i`", "lexInterpolatedString", + "iq{", "lexInterpolatedString", "q\"", "lexDelimitedString", "q{", "lexTokenString", "r\"", "lexWysiwygString", @@ -179,8 +183,8 @@ mixin template TokenTriviaFields() void toString(R)(auto ref R sink) const { - import std.conv : to; import dparse.lexer : str; + import std.conv : to; sink.put("tok!\""); sink.put(str(type)); @@ -641,11 +645,37 @@ public struct DLexer /// public void popFront()() pure nothrow @safe + { + if (range.index >= range.bytes.length) + { + _front.type = _tok!"\0"; + return; + } + + if (istringStack.length && istringStack[$ - 1].parens == 0) + { + _popFrontIstringContent(); + } + else + { + _popFrontNoIstring(); + } + } + + private void _popFrontNoIstring() pure nothrow @safe { do _popFront(); while (config.whitespaceBehavior == WhitespaceBehavior.skip && _front.type == tok!"whitespace"); + + if (istringStack.length) + { + if (_front.type == tok!"(") + istringStack[$ - 1].parens++; + else if (_front.type == tok!")") + istringStack[$ - 1].parens--; + } } /** @@ -1392,6 +1422,12 @@ private pure nothrow @safe: index); } + private ubyte lexStringSuffix() pure nothrow @safe + { + IdType t; + return lexStringSuffix(t); + } + private ubyte lexStringSuffix(ref IdType type) pure nothrow @safe { if (range.index >= range.bytes.length) @@ -1411,6 +1447,148 @@ private pure nothrow @safe: } } + void lexInterpolatedString(ref Token token) + { + mixin (tokenStart); + IstringState.Type type; + range.popFront(); + switch (range.bytes[range.index]) + { + case '"': type = IstringState.type.quote; break; + case '`': type = IstringState.type.backtick; break; + case 'q': + type = IstringState.type.tokenString; + range.popFront(); + break; + default: + assert(false); + } + range.popFront(); + token = Token(tok!"istringLiteralStart", cache.intern(range.slice(mark)), line, column, index); + istringStack ~= IstringState(0, 0, type); + } + + void _popFrontIstringContent() + { + mixin (tokenStart); + + assert(istringStack.length > 0); + assert(istringStack[$ - 1].parens == 0); + + if (istringStack[$ - 1].dollar) + { + assert(range.front == '(', "shouldn't be in dollar state without opening parens following it"); + istringStack[$ - 1].dollar = false; + istringStack[$ - 1].parens++; + range.popFront(); + _front = Token(tok!"(", null, line, column, index); + return; + } + + switch (range.front) + { + case '$': + if (isAtIstringExpression) + { + istringStack[$ - 1].dollar = true; + range.popFront(); + _front = Token(tok!"$", null, line, column, index); + break; + } + else + goto default; + case '}': + case '"': + case '`': + if (range.front != istringStack[$ - 1].type || istringStack[$ - 1].braces) + goto default; + + istringStack.length--; + range.popFront(); + lexStringSuffix(); + _front = Token(tok!"istringLiteralEnd", cache.intern(range.slice(mark)), line, + column, index); + break; + default: + _popFrontIstringPlain(); + break; + } + } + + void _popFrontIstringPlain() + { + mixin (tokenStart); + Loop: while (!range.empty) + { + if (istringStack[$ - 1].type == IstringState.Type.tokenString) + { + char c = range.bytes[range.index]; + switch (c) + { + case '$': + if (isAtIstringExpression) + break Loop; + else + goto default; + case '{': + istringStack[$ - 1].braces++; + popFrontWhitespaceAware(); + continue Loop; + case '}': + if (istringStack[$ - 1].braces == 0) + break Loop; + istringStack[$ - 1].braces--; + popFrontWhitespaceAware(); + continue Loop; + default: + break; + } + + _popFrontNoIstring(); + + if (range.index >= range.bytes.length) + { + error(_front, "Error: unterminated interpolated string token string literal"); + return; + } + } + else + { + char c = range.bytes[range.index]; + switch (c) + { + case '\\': + if (istringStack[$ - 1].type == IstringState.Type.quote) + lexEscapeSequence(); + else + goto default; + break; + case '$': + if (isAtIstringExpression) + break Loop; + else + goto default; + case '"': + case '`': + if (c == istringStack[$ - 1].type) + break Loop; + goto default; + default: + popFrontWhitespaceAware(); + break; + } + } + } + _front = Token(tok!"istringLiteralText", cache.intern(range.slice(mark)), + line, column, index); + } + + bool isAtIstringExpression() + { + return range.index + 1 < range.bytes.length + && range.bytes[range.index + 1] == '('; + } + void lexDelimitedString(ref Token token) { mixin (tokenStart); @@ -1548,7 +1726,7 @@ private pure nothrow @safe: config.stringBehavior = oldString; } - advance(_front); + popFront(); if (range.index >= range.bytes.length) { @@ -1652,6 +1830,7 @@ private pure nothrow @safe: case '\'': case '"': case '?': + case '$': case '\\': case 'a': case 'b': @@ -1958,6 +2137,22 @@ private pure nothrow @safe: StringCache* cache; LexerConfig config; bool haveSSE42; + IstringState[] istringStack; + + static struct IstringState + { + enum Type : ubyte + { + quote = '"', + backtick = '`', + tokenString = '}', + } + + ushort parens; + ushort braces; + Type type; + bool dollar; + } } /** @@ -2260,11 +2455,53 @@ private extern(C) void free(void*) nothrow pure @nogc @trusted; unittest { - auto source = cast(ubyte[]) q{ import std.stdio;}c; - auto tokens = getTokensForParser(source, LexerConfig(), - new StringCache(StringCache.defaultBucketCount)); - assert (tokens.map!"a.type"().equal([tok!"import", tok!"identifier", tok!".", - tok!"identifier", tok!";"])); + import std.conv; + auto tokens(string source) + { + auto tokens = getTokensForParser(cast(ubyte[]) source, LexerConfig(), + new StringCache(StringCache.defaultBucketCount)); + return tokens; + } + assert (tokens(q{ import std.stdio;}c).map!"a.type"().equal( + [tok!"import", tok!"identifier", tok!".", tok!"identifier", tok!";"])); + + assert (tokens(`i"hello".foo`).map!"a.type"().equal( + [ + tok!"istringLiteralStart", + tok!"istringLiteralText", + tok!"istringLiteralEnd", + tok!".", + tok!"identifier" + ]), tokens(`i"hello".foo`).to!string); + + assert (tokens(`i"hello $(name)".foo`).map!"a.type"().equal( + [ + tok!"istringLiteralStart", + tok!"istringLiteralText", + tok!"$", + tok!"(", + tok!"identifier", + tok!")", + tok!"istringLiteralEnd", + tok!".", + tok!"identifier" + ])); + + assert (tokens(`i"hello $(x + "hello $(world)") bar".foo`).map!"a.type"().equal( + [ + tok!"istringLiteralStart", + tok!"istringLiteralText", + tok!"$", + tok!"(", + tok!"identifier", + tok!"+", + tok!"stringLiteral", + tok!")", + tok!"istringLiteralText", + tok!"istringLiteralEnd", + tok!".", + tok!"identifier" + ])); } /// Test \x char sequence @@ -2584,12 +2821,12 @@ void main() { assert(tokens[i++].type == tok!";"); assert(tokens[i++].type == tok!"}"); - void checkInvalidTrailingString(const Token[] tokens) + void checkInvalidTrailingString(const Token[] tokens, int expected = 3) { - assert(tokens.length == 3); - assert(tokens[2].index != 0); - assert(tokens[2].column >= 4); - assert(tokens[2].type == tok!""); + assert(tokens.length == expected); + assert(tokens[$ - 1].index != 0); + assert(tokens[$ - 1].column >= 4); + assert(tokens[$ - 1].type == tok!""); } checkInvalidTrailingString(getTokensForParser(`x = "foo`, cf, &ca)); @@ -2599,4 +2836,100 @@ void main() { checkInvalidTrailingString(getTokensForParser("x = q{foo", cf, &ca)); checkInvalidTrailingString(getTokensForParser(`x = q"foo`, cf, &ca)); checkInvalidTrailingString(getTokensForParser("x = '", cf, &ca)); + checkInvalidTrailingString(getTokensForParser(`i"$("`, cf, &ca), 4); + checkInvalidTrailingString(getTokensForParser(`i"$("foo`, cf, &ca), 4); + checkInvalidTrailingString(getTokensForParser(`i"$(q{`, cf, &ca), 4); + checkInvalidTrailingString(getTokensForParser(`i"$(q{foo`, cf, &ca), 4); +} + +unittest +{ + import std.conv; + + auto test(string content, bool debugPrint = false) + { + LexerConfig cf; + StringCache ca = StringCache(16); + + const tokens = getTokensForParser(content, cf, &ca); + if (debugPrint) + return tokens.to!(char[][]).join("\n"); + + char[] ret = new char[content.length]; + ret[] = ' '; + foreach_reverse (t; tokens) + { + ret[t.index .. t.index + max(1, t.text.length)] = + t.type == tok!"$" ? '$' : + t.type == tok!"(" ? '(' : + t.type == tok!")" ? ')' : + t.type == tok!"{" ? '{' : + t.type == tok!"}" ? '}' : + t.type == tok!"identifier" ? 'i' : + t.type == tok!"istringLiteralStart" ? 'S' : + t.type == tok!"istringLiteralText" ? '.' : + t.type == tok!"istringLiteralEnd" ? 'E' : + t.type == tok!"" ? '%' : + '?'; + } + return ret; + } + + // dfmt off + + assert(test(`i"$name"`) + == `SS.....E`); + + assert(test(`i"\$plain\0"`) + == `SS.........E`); + + assert(test(`i"$(expression)"w`) + == `SS$(iiiiiiiiii)EE`); + + assert(test(`i"$(expression"c`) + == `SS$(iiiiiiiiii `); + + assert(test(`i"$name "`) + == `SS......E`); + + assert(test(`i"$ {}plain"`) + == `SS.........E`); + + assert(test("i\"$ ``plain\"") + == `SS.........E`); + + assert(test(`i"$0 plain"`) + == `SS........E`); + + assert(test(`i"\$0 plain"`) + == `SS.........E`); + + assert(test(`i"$.1 plain"`) + == `SS.........E`); + + assert(test(`i"I have $$(money)"`) + == `SS........$(iiiii)E`); + + assert(test(`i"I have \$$(money)"`) + == `SS.........$(iiiii)E`); + + assert(test("i`I \"have\" $$(money)`") + == "SS..........$(iiiii)E"); + + assert(test(`iq{I have a token}`) + == "SSS..............E", test(`iq{I have a token}`)); + + assert(test("iq{I `\"have\"` $(money)}") + == "SSS...........$(iiiii)E"); + + assert(test("iq{I `\"have\"` $$(money)}") + == "SSS............$(iiiii)E"); + + assert(test(`iq{I {} $(money)}`) + == "SSS.....$(iiiii)E", test(`iq{I {} $(money)}`)); + + assert(test(`iq{I {} $$(money)}`) + == "SSS......$(iiiii)E", test(`iq{I {} $$(money)}`)); + + // dfmt on } diff --git a/src/dparse/parser.d b/src/dparse/parser.d index 7b5ee722..ea67e7f3 100644 --- a/src/dparse/parser.d +++ b/src/dparse/parser.d @@ -2,15 +2,15 @@ module dparse.parser; -import dparse.lexer; import dparse.ast; +import dparse.lexer; import dparse.rollback_allocator; import dparse.stack_buffer; -import std.experimental.allocator.mallocator; -import std.experimental.allocator; -import std.conv; import std.algorithm; import std.array; +import std.conv; +import std.experimental.allocator; +import std.experimental.allocator.mallocator; import std.string : format; // Uncomment this if you want ALL THE OUTPUT @@ -4617,6 +4617,88 @@ class Parser return parseInterfaceOrClass(node, startIndex); } + /** + * Parses an InterpolatedString + * + * $(GRAMMAR $(RULEDEF interpolatedString): + * $(LITERAL 'i"') $(RULE InterpolatedStringPart)* $(LITERAL '"') + * ;) + */ + InterpolatedString parseInterpolatedString() + { + mixin(traceEnterAndExit!(__FUNCTION__)); + auto startIndex = index; + auto node = allocator.make!InterpolatedString; + mixin(tokenCheck!"istringLiteralStart"); + StackBuffer parts; + while (moreTokens && !currentIs(tok!"istringLiteralEnd")) + { + if (auto c = parseInterpolatedStringPart()) + parts.put(c); + else if (moreTokens) + advance(); + else + { + error("Expected more interpolated string parts but got EOF ", false); + return null; + } + } + ownArray(node.parts, parts); + expect(tok!"istringLiteralEnd"); + + node.tokens = tokens[startIndex .. index]; + return node; + } + + /** + * Parses an InterpolatedStringPart + * + * $(GRAMMAR $(RULEDEF interpolatedStringPart): + * $(LITERAL '$') $(RULE identifier) + * | $(LITERAL '$') $(LITERAL '$(LPAREN)') $(RULE expression) $(LITERAL '$(RPAREN)') + * | $(RULE stringEscapeSequence) + * | $(RULE NOT:$(LPAREN)$(LITERAL '$') | $(LITERAL '"')$(RPAREN))+ + * ;) + */ + InterpolatedStringPart parseInterpolatedStringPart() + { + mixin(traceEnterAndExit!(__FUNCTION__)); + auto startIndex = index; + + InterpolatedStringPart node; + + if (currentIs(tok!"istringLiteralText")) + { + node = allocator.make!InterpolatedStringText; + advance(); + } + else if (currentIs(tok!"$")) + { + if (peekIs(tok!"(")) + { + advance(); + advance(); + auto expNode = allocator.make!InterpolatedStringExpression; + expNode.expression = parseExpression(); + node = expNode; + expect(tok!")"); + } + else + { + error("Unexpected token after dollar inside interpolated string literal"); + return null; + } + } + else + { + error("Unexpected token inside interpolated string literal"); + return null; + } + + node.tokens = tokens[startIndex .. index]; + return node; + } + /** * Parses an Invariant * @@ -5801,6 +5883,7 @@ class Parser * | $(LITERAL FloatLiteral) * | $(LITERAL StringLiteral)+ * | $(LITERAL CharacterLiteral) + * | $(LITERAL IstringLiteral) * ;) */ PrimaryExpression parsePrimaryExpression() @@ -5919,6 +6002,9 @@ class Parser case tok!"import": mixin(parseNodeQ!(`node.importExpression`, `ImportExpression`)); break; + case tok!"istringLiteralStart": + mixin(parseNodeQ!(`node.interpolatedString`, `InterpolatedString`)); + break; case tok!"this": case tok!"super": foreach (L; Literals) { case L: } @@ -7162,6 +7248,7 @@ class Parser * | $(LITERAL StringLiteral) * | $(LITERAL IntegerLiteral) * | $(LITERAL FloatLiteral) + * | $(LITERAL InterpolatedString) * | $(LITERAL '_true') * | $(LITERAL '_false') * | $(LITERAL '_null') @@ -7197,6 +7284,9 @@ class Parser foreach (C; BasicTypes) { case C: } node.token = advance(); break; + case tok!"istringLiteralStart": + node.istring = parseInterpolatedString(); + break; default: error(`Invalid template argument. (Try enclosing in parenthesis?)`); return null; diff --git a/test/ast_checks/interpolated_string.d b/test/ast_checks/interpolated_string.d new file mode 100644 index 00000000..b6289d24 --- /dev/null +++ b/test/ast_checks/interpolated_string.d @@ -0,0 +1,4 @@ +void foo() +{ + writeln(i"Hello name, you have $$(wealth) in your account right now"); +} diff --git a/test/ast_checks/interpolated_string.txt b/test/ast_checks/interpolated_string.txt new file mode 100644 index 00000000..93368523 --- /dev/null +++ b/test/ast_checks/interpolated_string.txt @@ -0,0 +1,5 @@ +//interpolatedString[@startQuote='i"'] +//interpolatedString[@endQuote='"'] +//interpolatedString/text[text()='Hello name, you have $'] +//interpolatedString/text[text()=' in your account right now'] +//interpolatedString/expression/unaryExpression diff --git a/test/run_tests.sh b/test/run_tests.sh index 1e267d29..c2d66093 100755 --- a/test/run_tests.sh +++ b/test/run_tests.sh @@ -88,12 +88,12 @@ if [[ ${BUILDKITE:-} != "true" ]]; then expectParseFailure=1 elif [[ "$line" =~ ^# ]]; then true # comment line - elif echo "$AST" | xmllint --xpath "${line}" - 2>/dev/null > /dev/null; then - ((currentPasses=currentPasses+1)) - else + elif echo "$AST" | xmllint --xpath "${line}" - 2>&1 | grep 'XPath set is empty' >/dev/null; then echo echo -e " ${RED}Check on line $lineCount of $queryFile failed.${NORMAL}" ((currentFailures=currentFailures+1)) + else + ((currentPasses=currentPasses+1)) fi ((lineCount=lineCount+1)) done < "$queryFile"