Skip to content

Commit 0d064fe

Browse files
CST: Handle interpolated strings (#239)
This PR adds support for serializing interpolated strings. The structure of interpolated strings is a bit special compared to other nodes: we store the string literal parts and the expression parts independently. The visitor must account for this so that parts are visited in the correct order. We implement a special printer for interpolated strings, similar to how we've implemented it for other strings. We are guaranteed that an interpolated string starts and ends with a string literal (potentially empty), so the leading trivia of the first literal and trailing trivia of the last literal become the trivia for the overall interpolated string. Note that the characters `` ` `` , `{` and `}` are not stored anywhere as part of the CST. To ensure correct trivia attachment to tokens, we need to ensure that the end position of the string part is computed correctly. We manually increment current position to do this.
1 parent c7a105f commit 0d064fe

File tree

7 files changed

+103
-5
lines changed

7 files changed

+103
-5
lines changed

batteries/syntax/ast_types.luau

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ export type AstExprBinary = {
153153
rhsoperand: AstExpr,
154154
}
155155

156+
export type AstExprInterpString = {
157+
tag: "interpolatedstring",
158+
strings: { Token<string> },
159+
expressions: { AstExpr },
160+
}
161+
156162
export type AstExpr =
157163
| AstExprGroup
158164
| AstExprConstantNil
@@ -169,6 +175,7 @@ export type AstExpr =
169175
| AstExprTable
170176
| AstExprUnary
171177
| AstExprBinary
178+
| AstExprInterpString
172179

173180
export type AstStatBlock = {
174181
tag: "block",

batteries/syntax/printer.luau

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,31 @@ local function printString(expr: T.AstExprConstantString): string
4646
return result
4747
end
4848

49+
local function printInterpolatedString(expr: T.AstExprInterpString): string
50+
local result = ""
51+
52+
for i = 1, #expr.strings do
53+
result ..= printTriviaList(expr.strings[i].leadingTrivia)
54+
if i == 1 then
55+
result ..= "`"
56+
else
57+
result ..= "}"
58+
end
59+
result ..= expr.strings[i].text
60+
61+
if i == #expr.strings then
62+
result ..= "`"
63+
result ..= printTriviaList(expr.strings[i].trailingTrivia)
64+
else
65+
result ..= "{"
66+
result ..= printTriviaList(expr.strings[i].trailingTrivia)
67+
result ..= printExpr(expr.expressions[i])
68+
end
69+
end
70+
71+
return result
72+
end
73+
4974
type PrintVisitor = visitor.Visitor & {
5075
result: string,
5176
}
@@ -69,6 +94,11 @@ local function printVisitor()
6994
return false
7095
end
7196

97+
printer.visitInterpolatedString = function(node: T.AstExprInterpString)
98+
printer.result ..= printInterpolatedString(node)
99+
return false
100+
end
101+
72102
return printer
73103
end
74104

batteries/syntax/visitor.luau

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type Visitor = {
3131
visitIndexName: (T.AstExprIndexName) -> boolean,
3232
visitIndexExpr: (T.AstExprIndexExpr) -> boolean,
3333
visitGroup: (T.AstExprGroup) -> boolean,
34+
visitInterpolatedString: (T.AstExprInterpString) -> boolean,
3435

3536
visitTypeReference: (T.AstTypeReference) -> boolean,
3637
visitTypeBoolean: (T.AstTypeSingletonBool) -> boolean,
@@ -80,6 +81,7 @@ local defaultVisitor: Visitor = {
8081
visitIndexName = alwaysVisit :: any,
8182
visitIndexExpr = alwaysVisit :: any,
8283
visitGroup = alwaysVisit :: any,
84+
visitInterpolatedString = alwaysVisit,
8385

8486
visitTypeReference = alwaysVisit :: any,
8587
visitTypeBoolean = alwaysVisit :: any,
@@ -404,6 +406,17 @@ local function visitGroup(node: T.AstExprGroup, visitor: Visitor)
404406
end
405407
end
406408

409+
local function visitInterpolatedString(node: T.AstExprInterpString, visitor: Visitor)
410+
if visitor.visitInterpolatedString(node) then
411+
for i = 1, #node.strings do
412+
visitToken(node.strings[i], visitor)
413+
if i < #node.expressions then
414+
visitExpression(node.expressions[i], visitor)
415+
end
416+
end
417+
end
418+
end
419+
407420
local function visitTypeReference(node: T.AstTypeReference, visitor: Visitor)
408421
if visitor.visitTypeReference(node) then
409422
if node.prefix then
@@ -486,6 +499,8 @@ function visitExpression(expression: T.AstExpr, visitor: Visitor)
486499
visitIndexExpr(expression, visitor)
487500
elseif expression.tag == "group" then
488501
visitGroup(expression, visitor)
502+
elseif expression.tag == "interpolatedstring" then
503+
visitInterpolatedString(expression, visitor)
489504
else
490505
exhaustiveMatch(expression.tag)
491506
end

luau/src/luau.cpp

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -979,21 +979,44 @@ struct AstSerialize : public Luau::AstVisitor
979979

980980
void serialize(Luau::AstExprInterpString* node)
981981
{
982+
const auto* cstNode = lookupCstNode<Luau::CstExprInterpString>(node);
983+
982984
lua_rawcheckstack(L, 3);
983985
lua_createtable(L, 0, preambleSize + 2);
984986

985987
serializeNodePreamble(node, "interpolatedstring");
986988

987989
lua_createtable(L, node->strings.size, 0);
990+
lua_createtable(L, node->expressions.size, 0);
991+
988992
for (size_t i = 0; i < node->strings.size; i++)
989993
{
990-
lua_pushlstring(L, node->strings.data[i].data, node->strings.data[i].size);
991-
lua_rawseti(L, -2, i + 1);
994+
if (cstNode)
995+
{
996+
auto position = i > 0 ? cstNode->stringPositions.data[i] : node->location.begin;
997+
serializeToken(position, std::string(cstNode->sourceStrings.data[i].data, cstNode->sourceStrings.data[i].size).data());
998+
999+
// Unlike normal tokens, interpolated string parts contain extra characters (`, } or {) that were not included during advancement
1000+
// For simplicity, lets set the current position manually. We don't have an end position for these parts, so we must compute
1001+
// If string part was single line, end position = current position + 2 (start and end character)
1002+
// If string parts was multi line, end position = current position + 1 (just end character)
1003+
if (position.line == currentPosition.line)
1004+
currentPosition.column += 2;
1005+
else
1006+
currentPosition.column += 1;
1007+
}
1008+
else
1009+
lua_pushlstring(L, node->strings.data[i].data, node->strings.data[i].size);
1010+
lua_rawseti(L, -3, i + 1);
1011+
1012+
if (i < node->expressions.size)
1013+
{
1014+
node->expressions.data[i]->visit(this);
1015+
lua_rawseti(L, -2, i + 1);
1016+
}
9921017
}
1018+
lua_setfield(L, -3, "expressions");
9931019
lua_setfield(L, -2, "strings");
994-
995-
serializeExprs(node->expressions);
996-
lua_setfield(L, -2, "expressions");
9971020
}
9981021

9991022
void serialize(Luau::AstExprError* node)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
local a = `simple`
2+
local b = `start{true}`
3+
local c = `start{true}end`
4+
local d = `start \
5+
{true} end`
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- stylua: ignore
2+
local x =
3+
`test`
4+
5+
-- stylua: ignore
6+
print(
7+
`test {subject} with {subject}`
8+
)

tests/testAstSerializer.spec.luau

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,21 @@ end
120120
local function test_roundtrippableAst()
121121
local files = {
122122
"examples/a.luau",
123+
"examples/ansi.luau",
123124
"examples/async_read.luau",
124125
"examples/b.luau",
125126
"examples/badisnan.luau",
127+
"examples/directories.luau",
126128
"examples/json.luau",
127129
"examples/main.luau",
128130
"examples/net_example.luau",
131+
"examples/parallel_serve_helper.luau",
132+
"examples/parallel_serve.luau",
133+
"examples/parallel_sort_helper.luau",
134+
"examples/parallel_sort.luau",
129135
"examples/parsing.luau",
136+
"examples/process_env.luau",
137+
"examples/spawn_example.luau",
130138
"examples/time_example.luau",
131139
"examples/writeFile.luau",
132140
"tests/astSerializerTests/assignment-1.luau",
@@ -135,6 +143,8 @@ local function test_roundtrippableAst()
135143
"tests/astSerializerTests/function-declaration-1.luau",
136144
"tests/astSerializerTests/function-declaration-2.luau",
137145
"tests/astSerializerTests/generic-for-loop-1.luau",
146+
"tests/astSerializerTests/interpolated-string-1.luau",
147+
"tests/astSerializerTests/interpolated-string-2.luau",
138148
"tests/astSerializerTests/local-function-declaration-1.luau",
139149
"tests/astSerializerTests/numeric-for-loop-1.luau",
140150
"tests/astSerializerTests/type-singletons-1.luau",

0 commit comments

Comments
 (0)