mirror of
https://github.com/luau-lang/luau.git
synced 2024-12-12 21:10:37 +00:00
String interpolation (#614)
Implements the string interpolation RFC (#165). Adds the string interpolation as per the RFC. ```lua local name = "world" print(`Hello {name}!`) -- Hello world! ``` Co-authored-by: Arseny Kapoulkine <arseny.kapoulkine@gmail.com> Co-authored-by: Alexander McCord <11488393+alexmccord@users.noreply.github.com>
This commit is contained in:
parent
0ce4c45436
commit
da9d8e8c60
27 changed files with 915 additions and 36 deletions
|
@ -107,6 +107,7 @@ struct TypeChecker
|
||||||
WithPredicate<TypeId> checkExpr(const ScopePtr& scope, const AstExprTypeAssertion& expr);
|
WithPredicate<TypeId> checkExpr(const ScopePtr& scope, const AstExprTypeAssertion& expr);
|
||||||
WithPredicate<TypeId> checkExpr(const ScopePtr& scope, const AstExprError& expr);
|
WithPredicate<TypeId> checkExpr(const ScopePtr& scope, const AstExprError& expr);
|
||||||
WithPredicate<TypeId> checkExpr(const ScopePtr& scope, const AstExprIfElse& expr, std::optional<TypeId> expectedType = std::nullopt);
|
WithPredicate<TypeId> checkExpr(const ScopePtr& scope, const AstExprIfElse& expr, std::optional<TypeId> expectedType = std::nullopt);
|
||||||
|
WithPredicate<TypeId> checkExpr(const ScopePtr& scope, const AstExprInterpString& expr);
|
||||||
|
|
||||||
TypeId checkExprTable(const ScopePtr& scope, const AstExprTable& expr, const std::vector<std::pair<TypeId, TypeId>>& fieldTypes,
|
TypeId checkExprTable(const ScopePtr& scope, const AstExprTable& expr, const std::vector<std::pair<TypeId, TypeId>>& fieldTypes,
|
||||||
std::optional<TypeId> expectedType);
|
std::optional<TypeId> expectedType);
|
||||||
|
|
|
@ -445,6 +445,14 @@ struct AstJsonEncoder : public AstVisitor
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void write(class AstExprInterpString* node)
|
||||||
|
{
|
||||||
|
writeNode(node, "AstExprInterpString", [&]() {
|
||||||
|
PROP(strings);
|
||||||
|
PROP(expressions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void write(class AstExprTable* node)
|
void write(class AstExprTable* node)
|
||||||
{
|
{
|
||||||
writeNode(node, "AstExprTable", [&]() {
|
writeNode(node, "AstExprTable", [&]() {
|
||||||
|
@ -888,6 +896,12 @@ struct AstJsonEncoder : public AstVisitor
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool visit(class AstExprInterpString* node) override
|
||||||
|
{
|
||||||
|
write(node);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool visit(class AstExprLocal* node) override
|
bool visit(class AstExprLocal* node) override
|
||||||
{
|
{
|
||||||
write(node);
|
write(node);
|
||||||
|
|
|
@ -206,6 +206,24 @@ static bool similar(AstExpr* lhs, AstExpr* rhs)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
CASE(AstExprIfElse) return similar(le->condition, re->condition) && similar(le->trueExpr, re->trueExpr) && similar(le->falseExpr, re->falseExpr);
|
CASE(AstExprIfElse) return similar(le->condition, re->condition) && similar(le->trueExpr, re->trueExpr) && similar(le->falseExpr, re->falseExpr);
|
||||||
|
CASE(AstExprInterpString)
|
||||||
|
{
|
||||||
|
if (le->strings.size != re->strings.size)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (le->expressions.size != re->expressions.size)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < le->strings.size; ++i)
|
||||||
|
if (le->strings.data[i].size != re->strings.data[i].size || memcmp(le->strings.data[i].data, re->strings.data[i].data, le->strings.data[i].size) != 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < le->expressions.size; ++i)
|
||||||
|
if (!similar(le->expressions.data[i], re->expressions.data[i]))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LUAU_ASSERT(!"Unknown expression type");
|
LUAU_ASSERT(!"Unknown expression type");
|
||||||
|
|
|
@ -511,6 +511,28 @@ struct Printer
|
||||||
writer.keyword("else");
|
writer.keyword("else");
|
||||||
visualize(*a->falseExpr);
|
visualize(*a->falseExpr);
|
||||||
}
|
}
|
||||||
|
else if (const auto& a = expr.as<AstExprInterpString>())
|
||||||
|
{
|
||||||
|
writer.symbol("`");
|
||||||
|
|
||||||
|
size_t index = 0;
|
||||||
|
|
||||||
|
for (const auto& string : a->strings)
|
||||||
|
{
|
||||||
|
writer.write(escape(std::string_view(string.data, string.size), /* escapeForInterpString = */ true));
|
||||||
|
|
||||||
|
if (index < a->expressions.size)
|
||||||
|
{
|
||||||
|
writer.symbol("{");
|
||||||
|
visualize(*a->expressions.data[index]);
|
||||||
|
writer.symbol("}");
|
||||||
|
}
|
||||||
|
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.symbol("`");
|
||||||
|
}
|
||||||
else if (const auto& a = expr.as<AstExprError>())
|
else if (const auto& a = expr.as<AstExprError>())
|
||||||
{
|
{
|
||||||
writer.symbol("(error-expr");
|
writer.symbol("(error-expr");
|
||||||
|
|
|
@ -1805,6 +1805,8 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
|
||||||
result = checkExpr(scope, *a);
|
result = checkExpr(scope, *a);
|
||||||
else if (auto a = expr.as<AstExprIfElse>())
|
else if (auto a = expr.as<AstExprIfElse>())
|
||||||
result = checkExpr(scope, *a, expectedType);
|
result = checkExpr(scope, *a, expectedType);
|
||||||
|
else if (auto a = expr.as<AstExprInterpString>())
|
||||||
|
result = checkExpr(scope, *a);
|
||||||
else
|
else
|
||||||
ice("Unhandled AstExpr?");
|
ice("Unhandled AstExpr?");
|
||||||
|
|
||||||
|
@ -2999,6 +3001,14 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
|
||||||
return {types.size() == 1 ? types[0] : addType(UnionTypeVar{std::move(types)})};
|
return {types.size() == 1 ? types[0] : addType(UnionTypeVar{std::move(types)})};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprInterpString& expr)
|
||||||
|
{
|
||||||
|
for (AstExpr* expr : expr.expressions)
|
||||||
|
checkExpr(scope, *expr);
|
||||||
|
|
||||||
|
return {stringType};
|
||||||
|
}
|
||||||
|
|
||||||
TypeId TypeChecker::checkLValue(const ScopePtr& scope, const AstExpr& expr)
|
TypeId TypeChecker::checkLValue(const ScopePtr& scope, const AstExpr& expr)
|
||||||
{
|
{
|
||||||
return checkLValueBinding(scope, expr);
|
return checkLValueBinding(scope, expr);
|
||||||
|
|
|
@ -134,6 +134,10 @@ public:
|
||||||
{
|
{
|
||||||
return visit((class AstExpr*)node);
|
return visit((class AstExpr*)node);
|
||||||
}
|
}
|
||||||
|
virtual bool visit(class AstExprInterpString* node)
|
||||||
|
{
|
||||||
|
return visit((class AstExpr*)node);
|
||||||
|
}
|
||||||
virtual bool visit(class AstExprError* node)
|
virtual bool visit(class AstExprError* node)
|
||||||
{
|
{
|
||||||
return visit((class AstExpr*)node);
|
return visit((class AstExpr*)node);
|
||||||
|
@ -732,6 +736,22 @@ public:
|
||||||
AstExpr* falseExpr;
|
AstExpr* falseExpr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class AstExprInterpString : public AstExpr
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
LUAU_RTTI(AstExprInterpString)
|
||||||
|
|
||||||
|
AstExprInterpString(const Location& location, const AstArray<AstArray<char>>& strings, const AstArray<AstExpr*>& expressions);
|
||||||
|
|
||||||
|
void visit(AstVisitor* visitor) override;
|
||||||
|
|
||||||
|
/// An interpolated string such as `foo{bar}baz` is represented as
|
||||||
|
/// an array of strings for "foo" and "bar", and an array of expressions for "baz".
|
||||||
|
/// `strings` will always have one more element than `expressions`.
|
||||||
|
AstArray<AstArray<char>> strings;
|
||||||
|
AstArray<AstExpr*> expressions;
|
||||||
|
};
|
||||||
|
|
||||||
class AstStatBlock : public AstStat
|
class AstStatBlock : public AstStat
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
|
@ -61,6 +61,12 @@ struct Lexeme
|
||||||
SkinnyArrow,
|
SkinnyArrow,
|
||||||
DoubleColon,
|
DoubleColon,
|
||||||
|
|
||||||
|
InterpStringBegin,
|
||||||
|
InterpStringMid,
|
||||||
|
InterpStringEnd,
|
||||||
|
// An interpolated string with no expressions (like `x`)
|
||||||
|
InterpStringSimple,
|
||||||
|
|
||||||
AddAssign,
|
AddAssign,
|
||||||
SubAssign,
|
SubAssign,
|
||||||
MulAssign,
|
MulAssign,
|
||||||
|
@ -80,6 +86,8 @@ struct Lexeme
|
||||||
BrokenString,
|
BrokenString,
|
||||||
BrokenComment,
|
BrokenComment,
|
||||||
BrokenUnicode,
|
BrokenUnicode,
|
||||||
|
BrokenInterpDoubleBrace,
|
||||||
|
|
||||||
Error,
|
Error,
|
||||||
|
|
||||||
Reserved_BEGIN,
|
Reserved_BEGIN,
|
||||||
|
@ -208,6 +216,11 @@ private:
|
||||||
Lexeme readLongString(const Position& start, int sep, Lexeme::Type ok, Lexeme::Type broken);
|
Lexeme readLongString(const Position& start, int sep, Lexeme::Type ok, Lexeme::Type broken);
|
||||||
Lexeme readQuotedString();
|
Lexeme readQuotedString();
|
||||||
|
|
||||||
|
Lexeme readInterpolatedStringBegin();
|
||||||
|
Lexeme readInterpolatedStringSection(Position start, Lexeme::Type formatType, Lexeme::Type endType);
|
||||||
|
|
||||||
|
void readBackslashInString();
|
||||||
|
|
||||||
std::pair<AstName, Lexeme::Type> readName();
|
std::pair<AstName, Lexeme::Type> readName();
|
||||||
|
|
||||||
Lexeme readNumber(const Position& start, unsigned int startOffset);
|
Lexeme readNumber(const Position& start, unsigned int startOffset);
|
||||||
|
@ -231,6 +244,14 @@ private:
|
||||||
|
|
||||||
bool skipComments;
|
bool skipComments;
|
||||||
bool readNames;
|
bool readNames;
|
||||||
|
|
||||||
|
enum class BraceType
|
||||||
|
{
|
||||||
|
InterpolatedString,
|
||||||
|
Normal
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<BraceType> braceStack;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline bool isSpace(char ch)
|
inline bool isSpace(char ch)
|
||||||
|
|
|
@ -228,6 +228,9 @@ private:
|
||||||
// TODO: Add grammar rules here?
|
// TODO: Add grammar rules here?
|
||||||
AstExpr* parseIfElseExpr();
|
AstExpr* parseIfElseExpr();
|
||||||
|
|
||||||
|
// stringinterp ::= <INTERP_BEGIN> exp {<INTERP_MID> exp} <INTERP_END>
|
||||||
|
AstExpr* parseInterpString();
|
||||||
|
|
||||||
// Name
|
// Name
|
||||||
std::optional<Name> parseNameOpt(const char* context = nullptr);
|
std::optional<Name> parseNameOpt(const char* context = nullptr);
|
||||||
Name parseName(const char* context = nullptr);
|
Name parseName(const char* context = nullptr);
|
||||||
|
@ -379,6 +382,7 @@ private:
|
||||||
std::vector<unsigned int> matchRecoveryStopOnToken;
|
std::vector<unsigned int> matchRecoveryStopOnToken;
|
||||||
|
|
||||||
std::vector<AstStat*> scratchStat;
|
std::vector<AstStat*> scratchStat;
|
||||||
|
std::vector<AstArray<char>> scratchString;
|
||||||
std::vector<AstExpr*> scratchExpr;
|
std::vector<AstExpr*> scratchExpr;
|
||||||
std::vector<AstExpr*> scratchExprAux;
|
std::vector<AstExpr*> scratchExprAux;
|
||||||
std::vector<AstName> scratchName;
|
std::vector<AstName> scratchName;
|
||||||
|
|
|
@ -35,6 +35,6 @@ bool equalsLower(std::string_view lhs, std::string_view rhs);
|
||||||
|
|
||||||
size_t hashRange(const char* data, size_t size);
|
size_t hashRange(const char* data, size_t size);
|
||||||
|
|
||||||
std::string escape(std::string_view s);
|
std::string escape(std::string_view s, bool escapeForInterpString = false);
|
||||||
bool isIdentifier(std::string_view s);
|
bool isIdentifier(std::string_view s);
|
||||||
} // namespace Luau
|
} // namespace Luau
|
||||||
|
|
|
@ -349,6 +349,22 @@ AstExprError::AstExprError(const Location& location, const AstArray<AstExpr*>& e
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AstExprInterpString::AstExprInterpString(const Location& location, const AstArray<AstArray<char>>& strings, const AstArray<AstExpr*>& expressions)
|
||||||
|
: AstExpr(ClassIndex(), location)
|
||||||
|
, strings(strings)
|
||||||
|
, expressions(expressions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void AstExprInterpString::visit(AstVisitor* visitor)
|
||||||
|
{
|
||||||
|
if (visitor->visit(this))
|
||||||
|
{
|
||||||
|
for (AstExpr* expr : expressions)
|
||||||
|
expr->visit(visitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AstExprError::visit(AstVisitor* visitor)
|
void AstExprError::visit(AstVisitor* visitor)
|
||||||
{
|
{
|
||||||
if (visitor->visit(this))
|
if (visitor->visit(this))
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
|
|
||||||
|
LUAU_FASTFLAG(LuauInterpolatedStringBaseSupport)
|
||||||
|
|
||||||
namespace Luau
|
namespace Luau
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -89,7 +91,18 @@ Lexeme::Lexeme(const Location& location, Type type, const char* data, size_t siz
|
||||||
, length(unsigned(size))
|
, length(unsigned(size))
|
||||||
, data(data)
|
, data(data)
|
||||||
{
|
{
|
||||||
LUAU_ASSERT(type == RawString || type == QuotedString || type == Number || type == Comment || type == BlockComment);
|
LUAU_ASSERT(
|
||||||
|
type == RawString
|
||||||
|
|| type == QuotedString
|
||||||
|
|| type == InterpStringBegin
|
||||||
|
|| type == InterpStringMid
|
||||||
|
|| type == InterpStringEnd
|
||||||
|
|| type == InterpStringSimple
|
||||||
|
|| type == BrokenInterpDoubleBrace
|
||||||
|
|| type == Number
|
||||||
|
|| type == Comment
|
||||||
|
|| type == BlockComment
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Lexeme::Lexeme(const Location& location, Type type, const char* name)
|
Lexeme::Lexeme(const Location& location, Type type, const char* name)
|
||||||
|
@ -160,6 +173,18 @@ std::string Lexeme::toString() const
|
||||||
case QuotedString:
|
case QuotedString:
|
||||||
return data ? format("\"%.*s\"", length, data) : "string";
|
return data ? format("\"%.*s\"", length, data) : "string";
|
||||||
|
|
||||||
|
case InterpStringBegin:
|
||||||
|
return data ? format("`%.*s{", length, data) : "the beginning of an interpolated string";
|
||||||
|
|
||||||
|
case InterpStringMid:
|
||||||
|
return data ? format("}%.*s{", length, data) : "the middle of an interpolated string";
|
||||||
|
|
||||||
|
case InterpStringEnd:
|
||||||
|
return data ? format("}%.*s`", length, data) : "the end of an interpolated string";
|
||||||
|
|
||||||
|
case InterpStringSimple:
|
||||||
|
return data ? format("`%.*s`", length, data) : "interpolated string";
|
||||||
|
|
||||||
case Number:
|
case Number:
|
||||||
return data ? format("'%.*s'", length, data) : "number";
|
return data ? format("'%.*s'", length, data) : "number";
|
||||||
|
|
||||||
|
@ -175,6 +200,9 @@ std::string Lexeme::toString() const
|
||||||
case BrokenComment:
|
case BrokenComment:
|
||||||
return "unfinished comment";
|
return "unfinished comment";
|
||||||
|
|
||||||
|
case BrokenInterpDoubleBrace:
|
||||||
|
return "'{{', which is invalid (did you mean '\\{'?)";
|
||||||
|
|
||||||
case BrokenUnicode:
|
case BrokenUnicode:
|
||||||
if (codepoint)
|
if (codepoint)
|
||||||
{
|
{
|
||||||
|
@ -515,6 +543,32 @@ Lexeme Lexer::readLongString(const Position& start, int sep, Lexeme::Type ok, Le
|
||||||
return Lexeme(Location(start, position()), broken);
|
return Lexeme(Location(start, position()), broken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Lexer::readBackslashInString()
|
||||||
|
{
|
||||||
|
LUAU_ASSERT(peekch() == '\\');
|
||||||
|
consume();
|
||||||
|
switch (peekch())
|
||||||
|
{
|
||||||
|
case '\r':
|
||||||
|
consume();
|
||||||
|
if (peekch() == '\n')
|
||||||
|
consume();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'z':
|
||||||
|
consume();
|
||||||
|
while (isSpace(peekch()))
|
||||||
|
consume();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
consume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Lexeme Lexer::readQuotedString()
|
Lexeme Lexer::readQuotedString()
|
||||||
{
|
{
|
||||||
Position start = position();
|
Position start = position();
|
||||||
|
@ -535,27 +589,7 @@ Lexeme Lexer::readQuotedString()
|
||||||
return Lexeme(Location(start, position()), Lexeme::BrokenString);
|
return Lexeme(Location(start, position()), Lexeme::BrokenString);
|
||||||
|
|
||||||
case '\\':
|
case '\\':
|
||||||
consume();
|
readBackslashInString();
|
||||||
switch (peekch())
|
|
||||||
{
|
|
||||||
case '\r':
|
|
||||||
consume();
|
|
||||||
if (peekch() == '\n')
|
|
||||||
consume();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 0:
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'z':
|
|
||||||
consume();
|
|
||||||
while (isSpace(peekch()))
|
|
||||||
consume();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
consume();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -568,6 +602,69 @@ Lexeme Lexer::readQuotedString()
|
||||||
return Lexeme(Location(start, position()), Lexeme::QuotedString, &buffer[startOffset], offset - startOffset - 1);
|
return Lexeme(Location(start, position()), Lexeme::QuotedString, &buffer[startOffset], offset - startOffset - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Lexeme Lexer::readInterpolatedStringBegin()
|
||||||
|
{
|
||||||
|
LUAU_ASSERT(peekch() == '`');
|
||||||
|
|
||||||
|
Position start = position();
|
||||||
|
consume();
|
||||||
|
|
||||||
|
return readInterpolatedStringSection(start, Lexeme::InterpStringBegin, Lexeme::InterpStringSimple);
|
||||||
|
}
|
||||||
|
|
||||||
|
Lexeme Lexer::readInterpolatedStringSection(Position start, Lexeme::Type formatType, Lexeme::Type endType)
|
||||||
|
{
|
||||||
|
unsigned int startOffset = offset;
|
||||||
|
|
||||||
|
while (peekch() != '`')
|
||||||
|
{
|
||||||
|
switch (peekch())
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
case '\r':
|
||||||
|
case '\n':
|
||||||
|
return Lexeme(Location(start, position()), Lexeme::BrokenString);
|
||||||
|
|
||||||
|
case '\\':
|
||||||
|
// Allow for \u{}, which would otherwise be consumed by looking for {
|
||||||
|
if (peekch(1) == 'u' && peekch(2) == '{')
|
||||||
|
{
|
||||||
|
consume(); // backslash
|
||||||
|
consume(); // u
|
||||||
|
consume(); // {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
readBackslashInString();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '{':
|
||||||
|
{
|
||||||
|
braceStack.push_back(BraceType::InterpolatedString);
|
||||||
|
|
||||||
|
if (peekch(1) == '{')
|
||||||
|
{
|
||||||
|
Lexeme brokenDoubleBrace = Lexeme(Location(start, position()), Lexeme::BrokenInterpDoubleBrace, &buffer[startOffset], offset - startOffset);
|
||||||
|
consume();
|
||||||
|
consume();
|
||||||
|
return brokenDoubleBrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lexeme lexemeOutput(Location(start, position()), Lexeme::InterpStringBegin, &buffer[startOffset], offset - startOffset);
|
||||||
|
consume();
|
||||||
|
return lexemeOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
consume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
consume();
|
||||||
|
|
||||||
|
return Lexeme(Location(start, position()), endType, &buffer[startOffset], offset - startOffset - 1);
|
||||||
|
}
|
||||||
|
|
||||||
Lexeme Lexer::readNumber(const Position& start, unsigned int startOffset)
|
Lexeme Lexer::readNumber(const Position& start, unsigned int startOffset)
|
||||||
{
|
{
|
||||||
LUAU_ASSERT(isDigit(peekch()));
|
LUAU_ASSERT(isDigit(peekch()));
|
||||||
|
@ -660,6 +757,36 @@ Lexeme Lexer::readNext()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case '{':
|
||||||
|
{
|
||||||
|
consume();
|
||||||
|
|
||||||
|
if (!braceStack.empty())
|
||||||
|
braceStack.push_back(BraceType::Normal);
|
||||||
|
|
||||||
|
return Lexeme(Location(start, 1), '{');
|
||||||
|
}
|
||||||
|
|
||||||
|
case '}':
|
||||||
|
{
|
||||||
|
consume();
|
||||||
|
|
||||||
|
if (braceStack.empty())
|
||||||
|
{
|
||||||
|
return Lexeme(Location(start, 1), '}');
|
||||||
|
}
|
||||||
|
|
||||||
|
const BraceType braceStackTop = braceStack.back();
|
||||||
|
braceStack.pop_back();
|
||||||
|
|
||||||
|
if (braceStackTop != BraceType::InterpolatedString)
|
||||||
|
{
|
||||||
|
return Lexeme(Location(start, 1), '}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return readInterpolatedStringSection(position(), Lexeme::InterpStringMid, Lexeme::InterpStringEnd);
|
||||||
|
}
|
||||||
|
|
||||||
case '=':
|
case '=':
|
||||||
{
|
{
|
||||||
consume();
|
consume();
|
||||||
|
@ -716,6 +843,15 @@ Lexeme Lexer::readNext()
|
||||||
case '\'':
|
case '\'':
|
||||||
return readQuotedString();
|
return readQuotedString();
|
||||||
|
|
||||||
|
case '`':
|
||||||
|
if (FFlag::LuauInterpolatedStringBaseSupport)
|
||||||
|
return readInterpolatedStringBegin();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
consume();
|
||||||
|
return Lexeme(Location(start, 1), '`');
|
||||||
|
}
|
||||||
|
|
||||||
case '.':
|
case '.':
|
||||||
consume();
|
consume();
|
||||||
|
|
||||||
|
@ -817,8 +953,6 @@ Lexeme Lexer::readNext()
|
||||||
|
|
||||||
case '(':
|
case '(':
|
||||||
case ')':
|
case ')':
|
||||||
case '{':
|
|
||||||
case '}':
|
|
||||||
case ']':
|
case ']':
|
||||||
case ';':
|
case ';':
|
||||||
case ',':
|
case ',':
|
||||||
|
|
|
@ -23,10 +23,14 @@ LUAU_FASTFLAGVARIABLE(LuauErrorDoubleHexPrefix, false)
|
||||||
LUAU_FASTFLAGVARIABLE(LuauLintParseIntegerIssues, false)
|
LUAU_FASTFLAGVARIABLE(LuauLintParseIntegerIssues, false)
|
||||||
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuaReportParseIntegerIssues, false)
|
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuaReportParseIntegerIssues, false)
|
||||||
|
|
||||||
|
LUAU_FASTFLAGVARIABLE(LuauInterpolatedStringBaseSupport, false)
|
||||||
|
|
||||||
bool lua_telemetry_parsed_out_of_range_bin_integer = false;
|
bool lua_telemetry_parsed_out_of_range_bin_integer = false;
|
||||||
bool lua_telemetry_parsed_out_of_range_hex_integer = false;
|
bool lua_telemetry_parsed_out_of_range_hex_integer = false;
|
||||||
bool lua_telemetry_parsed_double_prefix_hex_integer = false;
|
bool lua_telemetry_parsed_double_prefix_hex_integer = false;
|
||||||
|
|
||||||
|
#define ERROR_INVALID_INTERP_DOUBLE_BRACE "Double braces are not permitted within interpolated strings. Did you mean '\\{'?"
|
||||||
|
|
||||||
namespace Luau
|
namespace Luau
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -1567,6 +1571,12 @@ AstTypeOrPack Parser::parseSimpleTypeAnnotation(bool allowPack)
|
||||||
else
|
else
|
||||||
return {reportTypeAnnotationError(begin, {}, /*isMissing*/ false, "String literal contains malformed escape sequence")};
|
return {reportTypeAnnotationError(begin, {}, /*isMissing*/ false, "String literal contains malformed escape sequence")};
|
||||||
}
|
}
|
||||||
|
else if (lexer.current().type == Lexeme::InterpStringBegin || lexer.current().type == Lexeme::InterpStringSimple)
|
||||||
|
{
|
||||||
|
parseInterpString();
|
||||||
|
|
||||||
|
return {reportTypeAnnotationError(begin, {}, /*isMissing*/ false, "Interpolated string literals cannot be used as types")};
|
||||||
|
}
|
||||||
else if (lexer.current().type == Lexeme::BrokenString)
|
else if (lexer.current().type == Lexeme::BrokenString)
|
||||||
{
|
{
|
||||||
Location location = lexer.current().location;
|
Location location = lexer.current().location;
|
||||||
|
@ -2215,15 +2225,24 @@ AstExpr* Parser::parseSimpleExpr()
|
||||||
{
|
{
|
||||||
return parseNumber();
|
return parseNumber();
|
||||||
}
|
}
|
||||||
else if (lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString)
|
else if (lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString || (FFlag::LuauInterpolatedStringBaseSupport && lexer.current().type == Lexeme::InterpStringSimple))
|
||||||
{
|
{
|
||||||
return parseString();
|
return parseString();
|
||||||
}
|
}
|
||||||
|
else if (FFlag::LuauInterpolatedStringBaseSupport && lexer.current().type == Lexeme::InterpStringBegin)
|
||||||
|
{
|
||||||
|
return parseInterpString();
|
||||||
|
}
|
||||||
else if (lexer.current().type == Lexeme::BrokenString)
|
else if (lexer.current().type == Lexeme::BrokenString)
|
||||||
{
|
{
|
||||||
nextLexeme();
|
nextLexeme();
|
||||||
return reportExprError(start, {}, "Malformed string");
|
return reportExprError(start, {}, "Malformed string");
|
||||||
}
|
}
|
||||||
|
else if (lexer.current().type == Lexeme::BrokenInterpDoubleBrace)
|
||||||
|
{
|
||||||
|
nextLexeme();
|
||||||
|
return reportExprError(start, {}, ERROR_INVALID_INTERP_DOUBLE_BRACE);
|
||||||
|
}
|
||||||
else if (lexer.current().type == Lexeme::Dot3)
|
else if (lexer.current().type == Lexeme::Dot3)
|
||||||
{
|
{
|
||||||
if (functionStack.back().vararg)
|
if (functionStack.back().vararg)
|
||||||
|
@ -2614,11 +2633,11 @@ AstArray<AstTypeOrPack> Parser::parseTypeParams()
|
||||||
|
|
||||||
std::optional<AstArray<char>> Parser::parseCharArray()
|
std::optional<AstArray<char>> Parser::parseCharArray()
|
||||||
{
|
{
|
||||||
LUAU_ASSERT(lexer.current().type == Lexeme::QuotedString || lexer.current().type == Lexeme::RawString);
|
LUAU_ASSERT(lexer.current().type == Lexeme::QuotedString || lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::InterpStringSimple);
|
||||||
|
|
||||||
scratchData.assign(lexer.current().data, lexer.current().length);
|
scratchData.assign(lexer.current().data, lexer.current().length);
|
||||||
|
|
||||||
if (lexer.current().type == Lexeme::QuotedString)
|
if (lexer.current().type == Lexeme::QuotedString || lexer.current().type == Lexeme::InterpStringSimple)
|
||||||
{
|
{
|
||||||
if (!Lexer::fixupQuotedString(scratchData))
|
if (!Lexer::fixupQuotedString(scratchData))
|
||||||
{
|
{
|
||||||
|
@ -2645,6 +2664,70 @@ AstExpr* Parser::parseString()
|
||||||
return reportExprError(location, {}, "String literal contains malformed escape sequence");
|
return reportExprError(location, {}, "String literal contains malformed escape sequence");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AstExpr* Parser::parseInterpString()
|
||||||
|
{
|
||||||
|
TempVector<AstArray<char>> strings(scratchString);
|
||||||
|
TempVector<AstExpr*> expressions(scratchExpr);
|
||||||
|
|
||||||
|
Location startLocation = lexer.current().location;
|
||||||
|
|
||||||
|
do {
|
||||||
|
Lexeme currentLexeme = lexer.current();
|
||||||
|
LUAU_ASSERT(
|
||||||
|
currentLexeme.type == Lexeme::InterpStringBegin
|
||||||
|
|| currentLexeme.type == Lexeme::InterpStringMid
|
||||||
|
|| currentLexeme.type == Lexeme::InterpStringEnd
|
||||||
|
|| currentLexeme.type == Lexeme::InterpStringSimple
|
||||||
|
);
|
||||||
|
|
||||||
|
Location location = currentLexeme.location;
|
||||||
|
|
||||||
|
Location startOfBrace = Location(location.end, 1);
|
||||||
|
|
||||||
|
scratchData.assign(currentLexeme.data, currentLexeme.length);
|
||||||
|
|
||||||
|
if (!Lexer::fixupQuotedString(scratchData))
|
||||||
|
{
|
||||||
|
nextLexeme();
|
||||||
|
return reportExprError(startLocation, {}, "Interpolated string literal contains malformed escape sequence");
|
||||||
|
}
|
||||||
|
|
||||||
|
AstArray<char> chars = copy(scratchData);
|
||||||
|
|
||||||
|
nextLexeme();
|
||||||
|
|
||||||
|
strings.push_back(chars);
|
||||||
|
|
||||||
|
if (currentLexeme.type == Lexeme::InterpStringEnd || currentLexeme.type == Lexeme::InterpStringSimple)
|
||||||
|
{
|
||||||
|
AstArray<AstArray<char>> stringsArray = copy(strings);
|
||||||
|
AstArray<AstExpr*> expressionsArray = copy(expressions);
|
||||||
|
|
||||||
|
return allocator.alloc<AstExprInterpString>(startLocation, stringsArray, expressionsArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
AstExpr* expression = parseExpr();
|
||||||
|
|
||||||
|
expressions.push_back(expression);
|
||||||
|
|
||||||
|
switch (lexer.current().type)
|
||||||
|
{
|
||||||
|
case Lexeme::InterpStringBegin:
|
||||||
|
case Lexeme::InterpStringMid:
|
||||||
|
case Lexeme::InterpStringEnd:
|
||||||
|
break;
|
||||||
|
case Lexeme::BrokenInterpDoubleBrace:
|
||||||
|
nextLexeme();
|
||||||
|
return reportExprError(location, {}, ERROR_INVALID_INTERP_DOUBLE_BRACE);
|
||||||
|
case Lexeme::BrokenString:
|
||||||
|
nextLexeme();
|
||||||
|
return reportExprError(location, {}, "Malformed interpolated string, did you forget to add a '}'?");
|
||||||
|
default:
|
||||||
|
return reportExprError(location, {}, "Malformed interpolated string, got %s", lexer.current().toString().c_str());
|
||||||
|
}
|
||||||
|
} while (true);
|
||||||
|
}
|
||||||
|
|
||||||
AstExpr* Parser::parseNumber()
|
AstExpr* Parser::parseNumber()
|
||||||
{
|
{
|
||||||
Location start = lexer.current().location;
|
Location start = lexer.current().location;
|
||||||
|
|
|
@ -230,19 +230,25 @@ bool isIdentifier(std::string_view s)
|
||||||
return (s.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890_") == std::string::npos);
|
return (s.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890_") == std::string::npos);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string escape(std::string_view s)
|
std::string escape(std::string_view s, bool escapeForInterpString)
|
||||||
{
|
{
|
||||||
std::string r;
|
std::string r;
|
||||||
r.reserve(s.size() + 50); // arbitrary number to guess how many characters we'll be inserting
|
r.reserve(s.size() + 50); // arbitrary number to guess how many characters we'll be inserting
|
||||||
|
|
||||||
for (uint8_t c : s)
|
for (uint8_t c : s)
|
||||||
{
|
{
|
||||||
if (c >= ' ' && c != '\\' && c != '\'' && c != '\"')
|
if (c >= ' ' && c != '\\' && c != '\'' && c != '\"' && c != '`' && c != '{')
|
||||||
r += c;
|
r += c;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
r += '\\';
|
r += '\\';
|
||||||
|
|
||||||
|
if (escapeForInterpString && (c == '`' || c == '{'))
|
||||||
|
{
|
||||||
|
r += c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
switch (c)
|
switch (c)
|
||||||
{
|
{
|
||||||
case '\a':
|
case '\a':
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <bitset>
|
#include <bitset>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
LUAU_FASTINTVARIABLE(LuauCompileLoopUnrollThreshold, 25)
|
LUAU_FASTINTVARIABLE(LuauCompileLoopUnrollThreshold, 25)
|
||||||
|
@ -25,6 +27,8 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5)
|
||||||
|
|
||||||
LUAU_FASTFLAGVARIABLE(LuauCompileXEQ, false)
|
LUAU_FASTFLAGVARIABLE(LuauCompileXEQ, false)
|
||||||
|
|
||||||
|
LUAU_FASTFLAG(LuauInterpolatedStringBaseSupport)
|
||||||
|
|
||||||
LUAU_FASTFLAGVARIABLE(LuauCompileOptimalAssignment, false)
|
LUAU_FASTFLAGVARIABLE(LuauCompileOptimalAssignment, false)
|
||||||
|
|
||||||
LUAU_FASTFLAGVARIABLE(LuauCompileExtractK, false)
|
LUAU_FASTFLAGVARIABLE(LuauCompileExtractK, false)
|
||||||
|
@ -1585,6 +1589,76 @@ struct Compiler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void compileExprInterpString(AstExprInterpString* expr, uint8_t target, bool targetTemp)
|
||||||
|
{
|
||||||
|
size_t formatCapacity = 0;
|
||||||
|
for (AstArray<char> string : expr->strings)
|
||||||
|
{
|
||||||
|
formatCapacity += string.size + std::count(string.data, string.data + string.size, '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string formatString;
|
||||||
|
formatString.reserve(formatCapacity);
|
||||||
|
|
||||||
|
size_t stringsLeft = expr->strings.size;
|
||||||
|
|
||||||
|
for (AstArray<char> string : expr->strings)
|
||||||
|
{
|
||||||
|
if (memchr(string.data, '%', string.size))
|
||||||
|
{
|
||||||
|
for (size_t characterIndex = 0; characterIndex < string.size; ++characterIndex)
|
||||||
|
{
|
||||||
|
char character = string.data[characterIndex];
|
||||||
|
formatString.push_back(character);
|
||||||
|
|
||||||
|
if (character == '%')
|
||||||
|
formatString.push_back('%');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
formatString.append(string.data, string.size);
|
||||||
|
|
||||||
|
stringsLeft--;
|
||||||
|
|
||||||
|
if (stringsLeft > 0)
|
||||||
|
formatString += "%*";
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t formatStringSize = formatString.size();
|
||||||
|
|
||||||
|
// We can't use formatStringRef.data() directly, because short strings don't have their data
|
||||||
|
// pinned in memory, so when interpFormatStrings grows, these pointers will move and become invalid.
|
||||||
|
std::unique_ptr<char[]> formatStringPtr(new char[formatStringSize]);
|
||||||
|
memcpy(formatStringPtr.get(), formatString.data(), formatStringSize);
|
||||||
|
|
||||||
|
AstArray<char> formatStringArray{formatStringPtr.get(), formatStringSize};
|
||||||
|
interpStrings.emplace_back(std::move(formatStringPtr)); // invalidates formatStringPtr, but keeps formatStringArray intact
|
||||||
|
|
||||||
|
int32_t formatStringIndex = bytecode.addConstantString(sref(formatStringArray));
|
||||||
|
if (formatStringIndex < 0)
|
||||||
|
CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile");
|
||||||
|
|
||||||
|
RegScope rs(this);
|
||||||
|
|
||||||
|
uint8_t baseReg = allocReg(expr, uint8_t(2 + expr->expressions.size));
|
||||||
|
|
||||||
|
emitLoadK(baseReg, formatStringIndex);
|
||||||
|
|
||||||
|
for (size_t index = 0; index < expr->expressions.size; ++index)
|
||||||
|
compileExprTempTop(expr->expressions.data[index], uint8_t(baseReg + 2 + index));
|
||||||
|
|
||||||
|
BytecodeBuilder::StringRef formatMethod = sref(AstName("format"));
|
||||||
|
|
||||||
|
int32_t formatMethodIndex = bytecode.addConstantString(formatMethod);
|
||||||
|
if (formatMethodIndex < 0)
|
||||||
|
CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile");
|
||||||
|
|
||||||
|
bytecode.emitABC(LOP_NAMECALL, baseReg, baseReg, uint8_t(BytecodeBuilder::getStringHash(formatMethod)));
|
||||||
|
bytecode.emitAux(formatMethodIndex);
|
||||||
|
bytecode.emitABC(LOP_CALL, baseReg, uint8_t(expr->expressions.size + 2), 2);
|
||||||
|
bytecode.emitABC(LOP_MOVE, target, baseReg, 0);
|
||||||
|
}
|
||||||
|
|
||||||
static uint8_t encodeHashSize(unsigned int hashSize)
|
static uint8_t encodeHashSize(unsigned int hashSize)
|
||||||
{
|
{
|
||||||
size_t hashSizeLog2 = 0;
|
size_t hashSizeLog2 = 0;
|
||||||
|
@ -2059,6 +2133,10 @@ struct Compiler
|
||||||
{
|
{
|
||||||
compileExprIfElse(expr, target, targetTemp);
|
compileExprIfElse(expr, target, targetTemp);
|
||||||
}
|
}
|
||||||
|
else if (AstExprInterpString* interpString = node->as<AstExprInterpString>(); FFlag::LuauInterpolatedStringBaseSupport && interpString)
|
||||||
|
{
|
||||||
|
compileExprInterpString(interpString, target, targetTemp);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LUAU_ASSERT(!"Unknown expression type");
|
LUAU_ASSERT(!"Unknown expression type");
|
||||||
|
@ -3808,6 +3886,7 @@ struct Compiler
|
||||||
std::vector<Loop> loops;
|
std::vector<Loop> loops;
|
||||||
std::vector<InlineFrame> inlineFrames;
|
std::vector<InlineFrame> inlineFrames;
|
||||||
std::vector<Capture> captures;
|
std::vector<Capture> captures;
|
||||||
|
std::vector<std::unique_ptr<char[]>> interpStrings;
|
||||||
};
|
};
|
||||||
|
|
||||||
void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, const AstNameTable& names, const CompileOptions& inputOptions)
|
void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, const AstNameTable& names, const CompileOptions& inputOptions)
|
||||||
|
|
|
@ -349,6 +349,11 @@ struct ConstantVisitor : AstVisitor
|
||||||
if (cond.type != Constant::Type_Unknown)
|
if (cond.type != Constant::Type_Unknown)
|
||||||
result = cond.isTruthful() ? trueExpr : falseExpr;
|
result = cond.isTruthful() ? trueExpr : falseExpr;
|
||||||
}
|
}
|
||||||
|
else if (AstExprInterpString* expr = node->as<AstExprInterpString>())
|
||||||
|
{
|
||||||
|
for (AstExpr* expression : expr->expressions)
|
||||||
|
analyze(expression);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LUAU_ASSERT(!"Unknown expression type");
|
LUAU_ASSERT(!"Unknown expression type");
|
||||||
|
|
|
@ -215,6 +215,16 @@ struct CostVisitor : AstVisitor
|
||||||
{
|
{
|
||||||
return model(expr->condition) + model(expr->trueExpr) + model(expr->falseExpr) + 2;
|
return model(expr->condition) + model(expr->trueExpr) + model(expr->falseExpr) + 2;
|
||||||
}
|
}
|
||||||
|
else if (AstExprInterpString* expr = node->as<AstExprInterpString>())
|
||||||
|
{
|
||||||
|
// Baseline cost of string.format
|
||||||
|
Cost cost = 3;
|
||||||
|
|
||||||
|
for (AstExpr* innerExpression : expr->expressions)
|
||||||
|
cost += model(innerExpression);
|
||||||
|
|
||||||
|
return cost;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LUAU_ASSERT(!"Unknown expression type");
|
LUAU_ASSERT(!"Unknown expression type");
|
||||||
|
|
|
@ -44,7 +44,8 @@ functioncall = prefixexp funcargs | prefixexp ':' NAME funcargs
|
||||||
exp = (asexp | unop exp) { binop exp }
|
exp = (asexp | unop exp) { binop exp }
|
||||||
ifelseexp = 'if' exp 'then' exp {'elseif' exp 'then' exp} 'else' exp
|
ifelseexp = 'if' exp 'then' exp {'elseif' exp 'then' exp} 'else' exp
|
||||||
asexp = simpleexp ['::' Type]
|
asexp = simpleexp ['::' Type]
|
||||||
simpleexp = NUMBER | STRING | 'nil' | 'true' | 'false' | '...' | tableconstructor | 'function' body | prefixexp | ifelseexp
|
stringinterp = INTERP_BEGIN exp { INTERP_MID exp } INTERP_END
|
||||||
|
simpleexp = NUMBER | STRING | 'nil' | 'true' | 'false' | '...' | tableconstructor | 'function' body | prefixexp | ifelseexp | stringinterp
|
||||||
funcargs = '(' [explist] ')' | tableconstructor | STRING
|
funcargs = '(' [explist] ')' | tableconstructor | STRING
|
||||||
|
|
||||||
tableconstructor = '{' [fieldlist] '}'
|
tableconstructor = '{' [fieldlist] '}'
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
#include "Luau/Ast.h"
|
#include "Luau/Ast.h"
|
||||||
#include "Luau/AstJsonEncoder.h"
|
#include "Luau/AstJsonEncoder.h"
|
||||||
#include "Luau/Parser.h"
|
#include "Luau/Parser.h"
|
||||||
|
#include "ScopedFlags.h"
|
||||||
|
|
||||||
#include "doctest.h"
|
#include "doctest.h"
|
||||||
|
|
||||||
|
@ -175,6 +176,17 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstExprIfThen")
|
||||||
CHECK(toJson(statement) == expected);
|
CHECK(toJson(statement) == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstExprInterpString")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
AstStat* statement = expectParseStatement("local a = `var = {x}`");
|
||||||
|
|
||||||
|
std::string_view expected =
|
||||||
|
R"({"type":"AstStatLocal","location":"0,0 - 0,17","vars":[{"luauType":null,"name":"a","type":"AstLocal","location":"0,6 - 0,7"}],"values":[{"type":"AstExprInterpString","location":"0,10 - 0,17","strings":["var = ",""],"expressions":[{"type":"AstExprGlobal","location":"0,18 - 0,19","global":"x"}]}]})";
|
||||||
|
|
||||||
|
CHECK(toJson(statement) == expected);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("encode_AstExprLocal")
|
TEST_CASE("encode_AstExprLocal")
|
||||||
{
|
{
|
||||||
|
|
|
@ -2708,6 +2708,15 @@ a = if temp then even else abc@3
|
||||||
CHECK(ac.entryMap.count("abcdef"));
|
CHECK(ac.entryMap.count("abcdef"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(ACFixture, "autocomplete_interpolated_string")
|
||||||
|
{
|
||||||
|
check(R"(f(`expression = {@1}`))");
|
||||||
|
|
||||||
|
auto ac = autocomplete('1');
|
||||||
|
CHECK(ac.entryMap.count("table"));
|
||||||
|
CHECK_EQ(ac.context, AutocompleteContext::Expression);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE_FIXTURE(ACFixture, "autocomplete_explicit_type_pack")
|
TEST_CASE_FIXTURE(ACFixture, "autocomplete_explicit_type_pack")
|
||||||
{
|
{
|
||||||
check(R"(
|
check(R"(
|
||||||
|
|
|
@ -1230,6 +1230,58 @@ RETURN R0 0
|
||||||
)");
|
)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("InterpStringWithNoExpressions")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
CHECK_EQ(compileFunction0(R"(return "hello")"), compileFunction0("return `hello`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("InterpStringZeroCost")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
CHECK_EQ(
|
||||||
|
"\n" + compileFunction0(R"(local _ = `hello, {"world"}!`)"),
|
||||||
|
R"(
|
||||||
|
LOADK R1 K0
|
||||||
|
LOADK R3 K1
|
||||||
|
NAMECALL R1 R1 K2
|
||||||
|
CALL R1 2 1
|
||||||
|
MOVE R0 R1
|
||||||
|
RETURN R0 0
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("InterpStringRegisterCleanup")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
CHECK_EQ(
|
||||||
|
"\n" + compileFunction0(R"(
|
||||||
|
local a, b, c = nil, "um", "uh oh"
|
||||||
|
a = `foo{"bar"}`
|
||||||
|
print(a)
|
||||||
|
)"),
|
||||||
|
|
||||||
|
R"(
|
||||||
|
LOADNIL R0
|
||||||
|
LOADK R1 K0
|
||||||
|
LOADK R2 K1
|
||||||
|
LOADK R3 K2
|
||||||
|
LOADK R5 K3
|
||||||
|
NAMECALL R3 R3 K4
|
||||||
|
CALL R3 2 1
|
||||||
|
MOVE R0 R3
|
||||||
|
GETIMPORT R3 6
|
||||||
|
MOVE R4 R0
|
||||||
|
CALL R3 1 0
|
||||||
|
RETURN R0 0
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("ConstantFoldArith")
|
TEST_CASE("ConstantFoldArith")
|
||||||
{
|
{
|
||||||
CHECK_EQ("\n" + compileFunction0("return 10 + 2"), R"(
|
CHECK_EQ("\n" + compileFunction0("return 10 + 2"), R"(
|
||||||
|
|
|
@ -294,6 +294,14 @@ TEST_CASE("Strings")
|
||||||
runConformance("strings.lua");
|
runConformance("strings.lua");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("StringInterp")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sffInterpStrings{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
ScopedFastFlag sffTostringFormat{"LuauTostringFormatSpecifier", true};
|
||||||
|
|
||||||
|
runConformance("stringinterp.lua");
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("VarArg")
|
TEST_CASE("VarArg")
|
||||||
{
|
{
|
||||||
runConformance("vararg.lua");
|
runConformance("vararg.lua");
|
||||||
|
|
|
@ -138,4 +138,90 @@ TEST_CASE("lookahead")
|
||||||
CHECK_EQ(lexer.lookahead().type, Lexeme::Eof);
|
CHECK_EQ(lexer.lookahead().type, Lexeme::Eof);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("string_interpolation_basic")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
const std::string testInput = R"(`foo {"bar"}`)";
|
||||||
|
Luau::Allocator alloc;
|
||||||
|
AstNameTable table(alloc);
|
||||||
|
Lexer lexer(testInput.c_str(), testInput.size(), table);
|
||||||
|
|
||||||
|
Lexeme interpBegin = lexer.next();
|
||||||
|
CHECK_EQ(interpBegin.type, Lexeme::InterpStringBegin);
|
||||||
|
|
||||||
|
Lexeme quote = lexer.next();
|
||||||
|
CHECK_EQ(quote.type, Lexeme::QuotedString);
|
||||||
|
|
||||||
|
Lexeme interpEnd = lexer.next();
|
||||||
|
CHECK_EQ(interpEnd.type, Lexeme::InterpStringEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("string_interpolation_double_brace")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
const std::string testInput = R"(`foo{{bad}}bar`)";
|
||||||
|
Luau::Allocator alloc;
|
||||||
|
AstNameTable table(alloc);
|
||||||
|
Lexer lexer(testInput.c_str(), testInput.size(), table);
|
||||||
|
|
||||||
|
auto brokenInterpBegin = lexer.next();
|
||||||
|
CHECK_EQ(brokenInterpBegin.type, Lexeme::BrokenInterpDoubleBrace);
|
||||||
|
CHECK_EQ(std::string(brokenInterpBegin.data, brokenInterpBegin.length), std::string("foo"));
|
||||||
|
|
||||||
|
CHECK_EQ(lexer.next().type, Lexeme::Name);
|
||||||
|
|
||||||
|
auto interpEnd = lexer.next();
|
||||||
|
CHECK_EQ(interpEnd.type, Lexeme::InterpStringEnd);
|
||||||
|
CHECK_EQ(std::string(interpEnd.data, interpEnd.length), std::string("}bar"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("string_interpolation_double_but_unmatched_brace")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
const std::string testInput = R"(`{{oops}`, 1)";
|
||||||
|
Luau::Allocator alloc;
|
||||||
|
AstNameTable table(alloc);
|
||||||
|
Lexer lexer(testInput.c_str(), testInput.size(), table);
|
||||||
|
|
||||||
|
CHECK_EQ(lexer.next().type, Lexeme::BrokenInterpDoubleBrace);
|
||||||
|
CHECK_EQ(lexer.next().type, Lexeme::Name);
|
||||||
|
CHECK_EQ(lexer.next().type, Lexeme::InterpStringEnd);
|
||||||
|
CHECK_EQ(lexer.next().type, ',');
|
||||||
|
CHECK_EQ(lexer.next().type, Lexeme::Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("string_interpolation_unmatched_brace")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
const std::string testInput = R"({
|
||||||
|
`hello {"world"}
|
||||||
|
} -- this might be incorrectly parsed as a string)";
|
||||||
|
Luau::Allocator alloc;
|
||||||
|
AstNameTable table(alloc);
|
||||||
|
Lexer lexer(testInput.c_str(), testInput.size(), table);
|
||||||
|
|
||||||
|
CHECK_EQ(lexer.next().type, '{');
|
||||||
|
CHECK_EQ(lexer.next().type, Lexeme::InterpStringBegin);
|
||||||
|
CHECK_EQ(lexer.next().type, Lexeme::QuotedString);
|
||||||
|
CHECK_EQ(lexer.next().type, Lexeme::BrokenString);
|
||||||
|
CHECK_EQ(lexer.next().type, '}');
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("string_interpolation_with_unicode_escape")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
const std::string testInput = R"(`\u{1F41B}`)";
|
||||||
|
Luau::Allocator alloc;
|
||||||
|
AstNameTable table(alloc);
|
||||||
|
Lexer lexer(testInput.c_str(), testInput.size(), table);
|
||||||
|
|
||||||
|
CHECK_EQ(lexer.next().type, Lexeme::InterpStringSimple);
|
||||||
|
CHECK_EQ(lexer.next().type, Lexeme::Eof);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_SUITE_END();
|
TEST_SUITE_END();
|
||||||
|
|
|
@ -1662,17 +1662,31 @@ TEST_CASE_FIXTURE(Fixture, "WrongCommentOptimize")
|
||||||
{
|
{
|
||||||
LintResult result = lint(R"(
|
LintResult result = lint(R"(
|
||||||
--!optimize
|
--!optimize
|
||||||
--!optimize
|
|
||||||
--!optimize me
|
--!optimize me
|
||||||
--!optimize 100500
|
--!optimize 100500
|
||||||
--!optimize 2
|
--!optimize 2
|
||||||
)");
|
)");
|
||||||
|
|
||||||
REQUIRE_EQ(result.warnings.size(), 4);
|
REQUIRE_EQ(result.warnings.size(), 3);
|
||||||
CHECK_EQ(result.warnings[0].text, "optimize directive requires an optimization level");
|
CHECK_EQ(result.warnings[0].text, "optimize directive requires an optimization level");
|
||||||
CHECK_EQ(result.warnings[1].text, "optimize directive requires an optimization level");
|
CHECK_EQ(result.warnings[1].text, "optimize directive uses unknown optimization level 'me', 0..2 expected");
|
||||||
CHECK_EQ(result.warnings[2].text, "optimize directive uses unknown optimization level 'me', 0..2 expected");
|
CHECK_EQ(result.warnings[2].text, "optimize directive uses unknown optimization level '100500', 0..2 expected");
|
||||||
CHECK_EQ(result.warnings[3].text, "optimize directive uses unknown optimization level '100500', 0..2 expected");
|
|
||||||
|
result = lint("--!optimize ");
|
||||||
|
REQUIRE_EQ(result.warnings.size(), 1);
|
||||||
|
CHECK_EQ(result.warnings[0].text, "optimize directive requires an optimization level");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "TestStringInterpolation")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
LintResult result = lint(R"(
|
||||||
|
--!nocheck
|
||||||
|
local _ = `unknown {foo}`
|
||||||
|
)");
|
||||||
|
|
||||||
|
REQUIRE_EQ(result.warnings.size(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE_FIXTURE(Fixture, "IntegerParsing")
|
TEST_CASE_FIXTURE(Fixture, "IntegerParsing")
|
||||||
|
|
|
@ -905,6 +905,146 @@ TEST_CASE_FIXTURE(Fixture, "parse_compound_assignment_error_multiple")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_double_brace_begin")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
parse(R"(
|
||||||
|
_ = `{{oops}}`
|
||||||
|
)");
|
||||||
|
FAIL("Expected ParseErrors to be thrown");
|
||||||
|
}
|
||||||
|
catch (const ParseErrors& e)
|
||||||
|
{
|
||||||
|
CHECK_EQ("Double braces are not permitted within interpolated strings. Did you mean '\\{'?", e.getErrors().front().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_double_brace_mid")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
parse(R"(
|
||||||
|
_ = `{nice} {{oops}}`
|
||||||
|
)");
|
||||||
|
FAIL("Expected ParseErrors to be thrown");
|
||||||
|
}
|
||||||
|
catch (const ParseErrors& e)
|
||||||
|
{
|
||||||
|
CHECK_EQ("Double braces are not permitted within interpolated strings. Did you mean '\\{'?", e.getErrors().front().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_without_end_brace")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
auto columnOfEndBraceError = [this](const char* code)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
parse(code);
|
||||||
|
FAIL("Expected ParseErrors to be thrown");
|
||||||
|
return UINT_MAX;
|
||||||
|
}
|
||||||
|
catch (const ParseErrors& e)
|
||||||
|
{
|
||||||
|
CHECK_EQ(e.getErrors().size(), 1);
|
||||||
|
|
||||||
|
auto error = e.getErrors().front();
|
||||||
|
CHECK_EQ("Malformed interpolated string, did you forget to add a '}'?", error.getMessage());
|
||||||
|
return error.getLocation().begin.column;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// This makes sure that the error is coming from the brace itself
|
||||||
|
CHECK_EQ(columnOfEndBraceError("_ = `{a`"), columnOfEndBraceError("_ = `{abcdefg`"));
|
||||||
|
CHECK_NE(columnOfEndBraceError("_ = `{a`"), columnOfEndBraceError("_ = `{a`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_without_end_brace_in_table")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
parse(R"(
|
||||||
|
_ = { `{a` }
|
||||||
|
)");
|
||||||
|
FAIL("Expected ParseErrors to be thrown");
|
||||||
|
}
|
||||||
|
catch (const ParseErrors& e)
|
||||||
|
{
|
||||||
|
CHECK_EQ(e.getErrors().size(), 2);
|
||||||
|
|
||||||
|
CHECK_EQ("Malformed interpolated string, did you forget to add a '}'?", e.getErrors().front().getMessage());
|
||||||
|
CHECK_EQ("Expected '}' (to close '{' at line 2), got <eof>", e.getErrors().back().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_mid_without_end_brace_in_table")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
parse(R"(
|
||||||
|
_ = { `x {"y"} {z` }
|
||||||
|
)");
|
||||||
|
FAIL("Expected ParseErrors to be thrown");
|
||||||
|
}
|
||||||
|
catch (const ParseErrors& e)
|
||||||
|
{
|
||||||
|
CHECK_EQ(e.getErrors().size(), 2);
|
||||||
|
|
||||||
|
CHECK_EQ("Malformed interpolated string, did you forget to add a '}'?", e.getErrors().front().getMessage());
|
||||||
|
CHECK_EQ("Expected '}' (to close '{' at line 2), got <eof>", e.getErrors().back().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_as_type_fail")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
parse(R"(
|
||||||
|
local a: `what` = `???`
|
||||||
|
local b: `what {"the"}` = `???`
|
||||||
|
local c: `what {"the"} heck` = `???`
|
||||||
|
)");
|
||||||
|
FAIL("Expected ParseErrors to be thrown");
|
||||||
|
}
|
||||||
|
catch (const ParseErrors& parseErrors)
|
||||||
|
{
|
||||||
|
CHECK_EQ(parseErrors.getErrors().size(), 3);
|
||||||
|
|
||||||
|
for (ParseError error : parseErrors.getErrors())
|
||||||
|
CHECK_EQ(error.getMessage(), "Interpolated string literals cannot be used as types");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_call_without_parens")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
parse(R"(
|
||||||
|
_ = print `{42}`
|
||||||
|
)");
|
||||||
|
FAIL("Expected ParseErrors to be thrown");
|
||||||
|
}
|
||||||
|
catch (const ParseErrors& e)
|
||||||
|
{
|
||||||
|
CHECK_EQ("Expected identifier when parsing expression, got `{", e.getErrors().front().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE_FIXTURE(Fixture, "parse_nesting_based_end_detection")
|
TEST_CASE_FIXTURE(Fixture, "parse_nesting_based_end_detection")
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#include "Luau/Transpiler.h"
|
#include "Luau/Transpiler.h"
|
||||||
|
|
||||||
#include "Fixture.h"
|
#include "Fixture.h"
|
||||||
|
#include "ScopedFlags.h"
|
||||||
|
|
||||||
#include "doctest.h"
|
#include "doctest.h"
|
||||||
|
|
||||||
|
@ -678,4 +679,22 @@ TEST_CASE_FIXTURE(Fixture, "transpile_for_in_multiple_types")
|
||||||
CHECK_EQ(code, transpile(code, {}, true).code);
|
CHECK_EQ(code, transpile(code, {}, true).code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "transpile_string_interp")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
std::string code = R"( local _ = `hello {name}` )";
|
||||||
|
|
||||||
|
CHECK_EQ(code, transpile(code, {}, true).code);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "transpile_string_literal_escape")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
std::string code = R"( local _ = ` bracket = \{, backtick = \` = {'ok'} ` )";
|
||||||
|
|
||||||
|
CHECK_EQ(code, transpile(code, {}, true).code);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_SUITE_END();
|
TEST_SUITE_END();
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include "Luau/VisitTypeVar.h"
|
#include "Luau/VisitTypeVar.h"
|
||||||
|
|
||||||
#include "Fixture.h"
|
#include "Fixture.h"
|
||||||
|
#include "ScopedFlags.h"
|
||||||
|
|
||||||
#include "doctest.h"
|
#include "doctest.h"
|
||||||
|
|
||||||
|
@ -828,6 +829,41 @@ end
|
||||||
LUAU_REQUIRE_NO_ERRORS(result);
|
LUAU_REQUIRE_NO_ERRORS(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "tc_interpolated_string_basic")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
CheckResult result = check(R"(
|
||||||
|
local foo: string = `hello {"world"}`
|
||||||
|
)");
|
||||||
|
|
||||||
|
LUAU_REQUIRE_NO_ERRORS(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "tc_interpolated_string_with_invalid_expression")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
CheckResult result = check(R"(
|
||||||
|
local function f(x: number) end
|
||||||
|
|
||||||
|
local foo: string = `hello {f("uh oh")}`
|
||||||
|
)");
|
||||||
|
|
||||||
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "tc_interpolated_string_constant_type")
|
||||||
|
{
|
||||||
|
ScopedFastFlag sff{"LuauInterpolatedStringBaseSupport", true};
|
||||||
|
|
||||||
|
CheckResult result = check(R"(
|
||||||
|
local foo: "hello" = `hello`
|
||||||
|
)");
|
||||||
|
|
||||||
|
LUAU_REQUIRE_NO_ERRORS(result);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If it wasn't instantly obvious, we have the fuzzer to thank for this gem of a test.
|
* If it wasn't instantly obvious, we have the fuzzer to thank for this gem of a test.
|
||||||
*
|
*
|
||||||
|
|
59
tests/conformance/stringinterp.lua
Normal file
59
tests/conformance/stringinterp.lua
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
local function assertEq(left, right)
|
||||||
|
assert(typeof(left) == "string", "left is a " .. typeof(left))
|
||||||
|
assert(typeof(right) == "string", "right is a " .. typeof(right))
|
||||||
|
|
||||||
|
if left ~= right then
|
||||||
|
error(string.format("%q ~= %q", left, right))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assertEq(`hello {"world"}`, "hello world")
|
||||||
|
assertEq(`Welcome {"to"} {"Luau"}!`, "Welcome to Luau!")
|
||||||
|
|
||||||
|
assertEq(`2 + 2 = {2 + 2}`, "2 + 2 = 4")
|
||||||
|
|
||||||
|
assertEq(`{1} {2} {3} {4} {5} {6} {7}`, "1 2 3 4 5 6 7")
|
||||||
|
|
||||||
|
local combo = {5, 2, 8, 9}
|
||||||
|
assertEq(`The lock combinations are: {table.concat(combo, ", ")}`, "The lock combinations are: 5, 2, 8, 9")
|
||||||
|
|
||||||
|
assertEq(`true = {true}`, "true = true")
|
||||||
|
|
||||||
|
local name = "Luau"
|
||||||
|
assertEq(`Welcome to {
|
||||||
|
name
|
||||||
|
}!`, "Welcome to Luau!")
|
||||||
|
|
||||||
|
local nameNotConstantEvaluated = (function() return "Luau" end)()
|
||||||
|
assertEq(`Welcome to {nameNotConstantEvaluated}!`, "Welcome to Luau!")
|
||||||
|
|
||||||
|
assertEq(`This {localName} does not exist`, "This nil does not exist")
|
||||||
|
|
||||||
|
assertEq(`Welcome to \
|
||||||
|
{name}!`, "Welcome to \nLuau!")
|
||||||
|
|
||||||
|
assertEq(`empty`, "empty")
|
||||||
|
|
||||||
|
assertEq(`Escaped brace: \{}`, "Escaped brace: {}")
|
||||||
|
assertEq(`Escaped brace \{} with {"expression"}`, "Escaped brace {} with expression")
|
||||||
|
assertEq(`Backslash \ that escapes the space is not a part of the string...`, "Backslash that escapes the space is not a part of the string...")
|
||||||
|
assertEq(`Escaped backslash \\`, "Escaped backslash \\")
|
||||||
|
assertEq(`Escaped backtick: \``, "Escaped backtick: `")
|
||||||
|
|
||||||
|
assertEq(`Hello {`from inside {"a nested string"}`}`, "Hello from inside a nested string")
|
||||||
|
|
||||||
|
assertEq(`1 {`2 {`3 {4}`}`}`, "1 2 3 4")
|
||||||
|
|
||||||
|
local health = 50
|
||||||
|
assert(`You have {health}% health` == "You have 50% health")
|
||||||
|
|
||||||
|
local function shadowsString(string)
|
||||||
|
return `Value is {string}`
|
||||||
|
end
|
||||||
|
|
||||||
|
assertEq(shadowsString("hello"), "Value is hello")
|
||||||
|
assertEq(shadowsString(1), "Value is 1")
|
||||||
|
|
||||||
|
assertEq(`\u{0041}\t`, "A\t")
|
||||||
|
|
||||||
|
return "OK"
|
Loading…
Reference in a new issue