From bba059907e2f0227d5718e85679aa75723a03144 Mon Sep 17 00:00:00 2001 From: WebFreak001 Date: Sat, 1 Mar 2025 00:43:43 +0100 Subject: [PATCH 1/6] interpolated strings, v2 better implementation, doing all the tokenizing at the start, an interpolated string being multiple tokens --- src/dparse/ast.d | 131 ++++++++- src/dparse/astprinter.d | 27 ++ src/dparse/formatter.d | 41 ++- src/dparse/lexer.d | 369 ++++++++++++++++++++++-- src/dparse/parser.d | 87 ++++++ test/ast_checks/interpolated_string.d | 4 + test/ast_checks/interpolated_string.txt | 7 + test/run_tests.sh | 6 +- 8 files changed, 648 insertions(+), 24 deletions(-) create mode 100644 test/ast_checks/interpolated_string.d create mode 100644 test/ast_checks/interpolated_string.txt diff --git a/src/dparse/ast.d b/src/dparse/ast.d index eba8d1a3..0d9f9af4 100644 --- a/src/dparse/ast.d +++ b/src/dparse/ast.d @@ -73,6 +73,9 @@ shared static this() typeMap[typeid(TypeofExpression)] = 46; typeMap[typeid(UnaryExpression)] = 47; typeMap[typeid(XorExpression)] = 48; + typeMap[typeid(InterpolatedStringExpression)] = 49; + typeMap[typeid(InterpolatedStringText)] = 50; + typeMap[typeid(InterpolatedStringVariable)] = 51; } /// Describes which syntax was used in a list of declarations in the containing AST node @@ -167,6 +170,19 @@ 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, 51 (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; + case 51: visit(cast(InterpolatedStringVariable) n); break; default: assert(false, __MODULE__ ~ " has a bug"); } } @@ -289,6 +305,10 @@ 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 InterpolatedStringVariable interpolatedStringVariable) { interpolatedStringVariable.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 +446,7 @@ template visitIfNotNull(fields ...) } } -mixin template OpEquals(bool print = false) +private mixin template OpEquals(extraFields...) { override bool opEquals(Object other) const { @@ -443,6 +463,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; @@ -2318,6 +2341,109 @@ 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"); +} + +/// +abstract class InterpolatedStringPart : BaseNode +{ +} + +/// +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"); +} + +/// +final class InterpolatedStringVariable : InterpolatedStringPart +{ + override void accept(ASTVisitor visitor) const + { + } + + /// The dollar token. + inout(Token) dollar() inout pure nothrow @nogc @safe scope + { + return tokens.length == 2 ? tokens[0] : Token.init; + } + + /// The variable name token. + inout(Token) name() inout pure nothrow @nogc @safe scope + { + return tokens.length == 2 ? tokens[1] : Token.init; + } + + mixin OpEquals!("name.text"); +} + +/// +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 +2924,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 +2944,7 @@ final class PrimaryExpression : ExpressionNode /** */ Type type; /** */ Token typeConstructor; /** */ Arguments arguments; + /** */ InterpolatedString interpolatedString; mixin OpEquals; } diff --git a/src/dparse/astprinter.d b/src/dparse/astprinter.d index ba17f6d6..edecca32 100644 --- a/src/dparse/astprinter.d +++ b/src/dparse/astprinter.d @@ -582,6 +582,33 @@ 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 InterpolatedStringVariable interpolatedStringVariable) + { + output.writeln("", xmlEscape(interpolatedStringVariable.name.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..8c923d70 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,36 @@ 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(InterpolatedStringVariable) part) format(cast(InterpolatedStringVariable) 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 InterpolatedStringVariable interpolatedStringVariable) + { + put("$"); + put(interpolatedStringVariable.name.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 +2602,7 @@ class Formatter(Sink) Type type; Token typeConstructor; Arguments arguments; + InterpolatedString interpolatedString; **/ with(primaryExpression) @@ -2606,6 +2637,7 @@ class Formatter(Sink) else if (vector) format(vector); else if (type) format(type); else if (arguments) format(arguments); + else if (interpolatedString) format(interpolatedString); } } @@ -4367,4 +4399,11 @@ 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};", "T x = iq{\n};"); } 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..86b345c9 100644 --- a/src/dparse/parser.d +++ b/src/dparse/parser.d @@ -4617,6 +4617,89 @@ 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 + advance(); + } + 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!"identifier")) + { + node = allocator.make!InterpolatedStringVariable; + advance(); + advance(); + } + else 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 +5884,7 @@ class Parser * | $(LITERAL FloatLiteral) * | $(LITERAL StringLiteral)+ * | $(LITERAL CharacterLiteral) + * | $(LITERAL IstringLiteral) * ;) */ PrimaryExpression parsePrimaryExpression() @@ -5919,6 +6003,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: } diff --git a/test/ast_checks/interpolated_string.d b/test/ast_checks/interpolated_string.d new file mode 100644 index 00000000..0c46fe73 --- /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..ce741e4d --- /dev/null +++ b/test/ast_checks/interpolated_string.txt @@ -0,0 +1,7 @@ +//interpolatedString[@startQuote='i"'] +//interpolatedString[@endQuote='"'] +//interpolatedString/text[text()='Hello '] +//interpolatedString/text[text()=', you have $'] +//interpolatedString/text[text()=' in your account right now'] +//interpolatedString/variable[text()='name'] +//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" From 51092ae3fdcc9f889a980cc66cbfeb1ece372671 Mon Sep 17 00:00:00 2001 From: WebFreak001 Date: Sat, 1 Mar 2025 01:13:28 +0100 Subject: [PATCH 2/6] add error handling for early istring end in parser --- src/dparse/parser.d | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/dparse/parser.d b/src/dparse/parser.d index 86b345c9..0fea2602 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 @@ -4635,8 +4635,13 @@ class Parser { if (auto c = parseInterpolatedStringPart()) parts.put(c); - else + else if (moreTokens) advance(); + else + { + error("Expected more interpolated string parts but got EOF ", false); + return null; + } } ownArray(node.parts, parts); expect(tok!"istringLiteralEnd"); From dddfc50337e7fbb3f5283827418b1a8984b61adf Mon Sep 17 00:00:00 2001 From: WebFreak001 Date: Sat, 1 Mar 2025 01:23:17 +0100 Subject: [PATCH 3/6] supported istring as TemplateSingleArgument --- src/dparse/ast.d | 11 ++++++----- src/dparse/formatter.d | 12 ++++++++++-- src/dparse/parser.d | 4 ++++ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/dparse/ast.d b/src/dparse/ast.d index 0d9f9af4..f27f65b5 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; @@ -1425,8 +1425,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, @@ -3483,9 +3483,10 @@ final class TemplateSingleArgument : BaseNode { override void accept(ASTVisitor visitor) const { - mixin (visitIfNotNull!(token)); + mixin (visitIfNotNull!(token, istring)); } /** */ Token token; + /** */ InterpolatedString istring; mixin OpEquals; } @@ -4092,7 +4093,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/formatter.d b/src/dparse/formatter.d index 8c923d70..8d282e66 100644 --- a/src/dparse/formatter.d +++ b/src/dparse/formatter.d @@ -3290,7 +3290,10 @@ class Formatter(Sink) **/ put("!"); - format(templateSingleArgument.token); + if (templateSingleArgument.istring) + format(templateSingleArgument.istring); + else + format(templateSingleArgument.token); } void format(const TemplateThisParameter templateThisParameter) @@ -4405,5 +4408,10 @@ z /// Documentation for z 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};", "T x = iq{\n};"); + testFormatNode!(VariableDeclaration)("T x = iq{\n};"); + testFormatNode!(AliasDeclaration)(`alias expr = AliasSeq!i"$(a) $(b)";`); + testFormatNode!(VariableDeclaration)(q{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/parser.d b/src/dparse/parser.d index 0fea2602..d158c694 100644 --- a/src/dparse/parser.d +++ b/src/dparse/parser.d @@ -7254,6 +7254,7 @@ class Parser * | $(LITERAL StringLiteral) * | $(LITERAL IntegerLiteral) * | $(LITERAL FloatLiteral) + * | $(LITERAL InterpolatedString) * | $(LITERAL '_true') * | $(LITERAL '_false') * | $(LITERAL '_null') @@ -7289,6 +7290,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; From c26f5b6e6270f4aeffd4059c6ab95c23c7627ea5 Mon Sep 17 00:00:00 2001 From: WebFreak001 Date: Sat, 1 Mar 2025 01:24:46 +0100 Subject: [PATCH 4/6] fix AST test --- test/ast_checks/interpolated_string.d | 2 +- test/ast_checks/interpolated_string.txt | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/test/ast_checks/interpolated_string.d b/test/ast_checks/interpolated_string.d index 0c46fe73..b6289d24 100644 --- a/test/ast_checks/interpolated_string.d +++ b/test/ast_checks/interpolated_string.d @@ -1,4 +1,4 @@ void foo() { - writeln(i"Hello $name, you have $$(wealth) in your account right now"); + 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 index ce741e4d..93368523 100644 --- a/test/ast_checks/interpolated_string.txt +++ b/test/ast_checks/interpolated_string.txt @@ -1,7 +1,5 @@ //interpolatedString[@startQuote='i"'] //interpolatedString[@endQuote='"'] -//interpolatedString/text[text()='Hello '] -//interpolatedString/text[text()=', you have $'] +//interpolatedString/text[text()='Hello name, you have $'] //interpolatedString/text[text()=' in your account right now'] -//interpolatedString/variable[text()='name'] //interpolatedString/expression/unaryExpression From fc94d4dfb72d348d08db45bed2a7147dced70297 Mon Sep 17 00:00:00 2001 From: WebFreak001 Date: Sat, 1 Mar 2025 01:29:40 +0100 Subject: [PATCH 5/6] make test work with non-IES supported compiler --- src/dparse/formatter.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dparse/formatter.d b/src/dparse/formatter.d index 8d282e66..6a48ba6b 100644 --- a/src/dparse/formatter.d +++ b/src/dparse/formatter.d @@ -4410,7 +4410,7 @@ z /// Documentation for z 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)(q{auto thing = i"$(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{{$('$')}};"); From 8cb4c1377656e6d726c8b2b25f2ae50daa4aeb33 Mon Sep 17 00:00:00 2001 From: WebFreak001 Date: Sat, 1 Mar 2025 01:34:08 +0100 Subject: [PATCH 6/6] Remove support for $identifier inside IES --- src/dparse/ast.d | 33 ++++----------------------------- src/dparse/astprinter.d | 5 ----- src/dparse/formatter.d | 7 ------- src/dparse/parser.d | 8 +------- 4 files changed, 5 insertions(+), 48 deletions(-) diff --git a/src/dparse/ast.d b/src/dparse/ast.d index f27f65b5..ebc24d36 100644 --- a/src/dparse/ast.d +++ b/src/dparse/ast.d @@ -75,7 +75,6 @@ shared static this() typeMap[typeid(XorExpression)] = 48; typeMap[typeid(InterpolatedStringExpression)] = 49; typeMap[typeid(InterpolatedStringText)] = 50; - typeMap[typeid(InterpolatedStringVariable)] = 51; } /// Describes which syntax was used in a list of declarations in the containing AST node @@ -170,7 +169,7 @@ 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, 51 (used for InterpolatedStringPart) + // skip 49, 50 (used for InterpolatedStringPart) default: assert(false, __MODULE__ ~ " has a bug"); } } @@ -182,7 +181,6 @@ abstract class ASTVisitor { case 49: visit(cast(InterpolatedStringExpression) n); break; case 50: visit(cast(InterpolatedStringText) n); break; - case 51: visit(cast(InterpolatedStringVariable) n); break; default: assert(false, __MODULE__ ~ " has a bug"); } } @@ -308,7 +306,6 @@ abstract class ASTVisitor /** */ 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 InterpolatedStringVariable interpolatedStringVariable) { interpolatedStringVariable.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); } @@ -2382,12 +2379,12 @@ final class InterpolatedString : BaseNode 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 @@ -2403,29 +2400,7 @@ final class InterpolatedStringText : InterpolatedStringPart mixin OpEquals!("text.text"); } -/// -final class InterpolatedStringVariable : InterpolatedStringPart -{ - override void accept(ASTVisitor visitor) const - { - } - - /// The dollar token. - inout(Token) dollar() inout pure nothrow @nogc @safe scope - { - return tokens.length == 2 ? tokens[0] : Token.init; - } - - /// The variable name token. - inout(Token) name() inout pure nothrow @nogc @safe scope - { - return tokens.length == 2 ? tokens[1] : Token.init; - } - - mixin OpEquals!("name.text"); -} - -/// +/// A $(...) interpolation sequence final class InterpolatedStringExpression : InterpolatedStringPart { override void accept(ASTVisitor visitor) const diff --git a/src/dparse/astprinter.d b/src/dparse/astprinter.d index edecca32..aae7c57b 100644 --- a/src/dparse/astprinter.d +++ b/src/dparse/astprinter.d @@ -599,11 +599,6 @@ class XMLPrinter : ASTVisitor output.writeln("", xmlEscape(interpolatedStringText.text.text), ""); } - override void visit(const InterpolatedStringVariable interpolatedStringVariable) - { - output.writeln("", xmlEscape(interpolatedStringVariable.name.text), ""); - } - override void visit(const InterpolatedStringExpression interpolatedStringExpression) { visit(interpolatedStringExpression.expression); diff --git a/src/dparse/formatter.d b/src/dparse/formatter.d index 6a48ba6b..77217439 100644 --- a/src/dparse/formatter.d +++ b/src/dparse/formatter.d @@ -2019,7 +2019,6 @@ class Formatter(Sink) foreach (part; interpolatedString.parts) { if (cast(InterpolatedStringText) part) format(cast(InterpolatedStringText) part); - else if (cast(InterpolatedStringVariable) part) format(cast(InterpolatedStringVariable) part); else if (cast(InterpolatedStringExpression) part) format(cast(InterpolatedStringExpression) part); } put(interpolatedString.endQuote.text); @@ -2030,12 +2029,6 @@ class Formatter(Sink) put(interpolatedStringText.text.text); } - void format(const InterpolatedStringVariable interpolatedStringVariable) - { - put("$"); - put(interpolatedStringVariable.name.text); - } - void format(const InterpolatedStringExpression interpolatedStringExpression) { put("$("); diff --git a/src/dparse/parser.d b/src/dparse/parser.d index d158c694..ea67e7f3 100644 --- a/src/dparse/parser.d +++ b/src/dparse/parser.d @@ -4674,13 +4674,7 @@ class Parser } else if (currentIs(tok!"$")) { - if (peekIs(tok!"identifier")) - { - node = allocator.make!InterpolatedStringVariable; - advance(); - advance(); - } - else if (peekIs(tok!"(")) + if (peekIs(tok!"(")) { advance(); advance();