LibJS: Implement tagged template literals (foobar)

To make processing tagged template literals easier, template literals
will now add one empty StringLiteral before and after each template
expression *if* there's no other string - e.g.:

`${foo}` -> "", foo, ""
`test${foo}${bar}test` -> "test", foo, "", bar, "test"

This also matches the behaviour of many other parsers.
This commit is contained in:
Linus Groh 2020-05-06 10:17:35 +01:00 committed by Andreas Kling
parent eea62dd365
commit 4d20cf57db
Notes: sideshowbarker 2024-07-19 06:56:19 +09:00
4 changed files with 161 additions and 3 deletions

View file

@ -1277,8 +1277,7 @@ Value ArrayExpression::execute(Interpreter& interpreter) const
void TemplateLiteral::dump(int indent) const
{
ASTNode::dump(indent);
for (auto& expression : expressions())
for (auto& expression : m_expressions)
expression.dump(indent + 1);
}
@ -1286,7 +1285,7 @@ Value TemplateLiteral::execute(Interpreter& interpreter) const
{
StringBuilder string_builder;
for (auto& expression : expressions()) {
for (auto& expression : m_expressions) {
auto expr = expression.execute(interpreter);
if (interpreter.exception())
return {};
@ -1296,6 +1295,45 @@ Value TemplateLiteral::execute(Interpreter& interpreter) const
return js_string(interpreter, string_builder.build());
}
void TaggedTemplateLiteral::dump(int indent) const
{
ASTNode::dump(indent);
print_indent(indent + 1);
printf("(Tag)\n");
m_tag->dump(indent + 2);
print_indent(indent + 1);
printf("(Template Literal)\n");
m_template_literal->dump(indent + 2);
}
Value TaggedTemplateLiteral::execute(Interpreter& interpreter) const
{
auto tag = m_tag->execute(interpreter);
if (interpreter.exception())
return {};
if (!tag.is_function()) {
interpreter.throw_exception<TypeError>(String::format("%s is not a function", tag.to_string().characters()));
return {};
}
auto& tag_function = tag.as_function();
auto& expressions = m_template_literal->expressions();
auto* strings = Array::create(interpreter.global_object());
MarkedValueList arguments(interpreter.heap());
arguments.append(strings);
for (size_t i = 0; i < expressions.size(); ++i) {
auto value = expressions[i].execute(interpreter);
if (interpreter.exception())
return {};
// tag`${foo}` -> "", foo, "" -> tag(["", ""], foo)
// tag`foo${bar}baz${qux}` -> "foo", bar, "baz", qux, "" -> tag(["foo", "baz", ""], bar, qux)
if (i % 2 == 0)
strings->elements().append(value);
else
arguments.append(value);
}
return interpreter.call(tag_function, js_undefined(), move(arguments));
}
void TryStatement::dump(int indent) const
{
ASTNode::dump(indent);

View file

@ -786,6 +786,24 @@ private:
const NonnullRefPtrVector<Expression> m_expressions;
};
class TaggedTemplateLiteral final : public Expression {
public:
TaggedTemplateLiteral(NonnullRefPtr<Expression> tag, NonnullRefPtr<TemplateLiteral> template_literal)
: m_tag(move(tag))
, m_template_literal(move(template_literal))
{
}
virtual Value execute(Interpreter&) const override;
virtual void dump(int indent) const override;
private:
virtual const char* class_name() const override { return "TaggedTemplateLiteral"; }
const NonnullRefPtr<Expression> m_tag;
const NonnullRefPtr<TemplateLiteral> m_template_literal;
};
class MemberExpression final : public Expression {
public:
MemberExpression(NonnullRefPtr<Expression> object, NonnullRefPtr<Expression> property, bool computed = false)

View file

@ -560,6 +560,9 @@ NonnullRefPtr<TemplateLiteral> Parser::parse_template_literal()
NonnullRefPtrVector<Expression> expressions;
if (!match(TokenType::TemplateLiteralString))
expressions.append(create_ast_node<StringLiteral>(""));
while (!match(TokenType::TemplateLiteralEnd) && !match(TokenType::UnterminatedTemplateLiteral)) {
if (match(TokenType::TemplateLiteralString)) {
expressions.append(create_ast_node<StringLiteral>(consume().string_value()));
@ -576,6 +579,9 @@ NonnullRefPtr<TemplateLiteral> Parser::parse_template_literal()
return create_ast_node<TemplateLiteral>(expressions);
}
consume(TokenType::TemplateLiteralExprEnd);
if (!match(TokenType::TemplateLiteralString))
expressions.append(create_ast_node<StringLiteral>(""));
}
}
@ -591,6 +597,10 @@ NonnullRefPtr<TemplateLiteral> Parser::parse_template_literal()
NonnullRefPtr<Expression> Parser::parse_expression(int min_precedence, Associativity associativity)
{
auto expression = parse_primary_expression();
while (match(TokenType::TemplateLiteralStart)) {
auto template_literal = parse_template_literal();
expression = create_ast_node<TaggedTemplateLiteral>(move(expression), move(template_literal));
}
while (match_secondary_expression()) {
int new_precedence = operator_precedence(m_parser_state.m_current_token.type());
if (new_precedence < min_precedence)
@ -600,6 +610,10 @@ NonnullRefPtr<Expression> Parser::parse_expression(int min_precedence, Associati
Associativity new_associativity = operator_associativity(m_parser_state.m_current_token.type());
expression = parse_secondary_expression(move(expression), new_precedence, new_associativity);
while (match(TokenType::TemplateLiteralStart)) {
auto template_literal = parse_template_literal();
expression = create_ast_node<TaggedTemplateLiteral>(move(expression), move(template_literal));
}
}
return expression;
}

View file

@ -0,0 +1,88 @@
load("test-common.js");
try {
assertThrowsError(() => {
foo`bar${baz}`;
}, {
error: ReferenceError,
message: "'foo' not known"
});
assertThrowsError(() => {
function foo() { }
foo`bar${baz}`;
}, {
error: ReferenceError,
message: "'baz' not known"
});
assertThrowsError(() => {
undefined``````;
}, {
error: TypeError,
message: "undefined is not a function"
});
function test1(strings) {
assert(strings instanceof Array);
assert(strings.length === 1);
assert(strings[0] === "");
return 42;
}
assert(test1`` === 42);
function test2(s) {
return function (strings) {
assert(strings instanceof Array);
assert(strings.length === 1);
assert(strings[0] === "bar");
return s + strings[0];
}
}
assert(test2("foo")`bar` === "foobar");
var test3 = {
foo(strings, p1) {
assert(strings instanceof Array);
assert(strings.length === 2);
assert(strings[0] === "");
assert(strings[1] === "");
assert(p1 === "bar");
}
};
test3.foo`${"bar"}`;
function test4(strings, p1) {
assert(strings instanceof Array);
assert(strings.length === 2);
assert(strings[0] === "foo");
assert(strings[1] === "");
assert(p1 === 42);
}
var bar = 42;
test4`foo${bar}`;
function test5(strings, p1, p2) {
assert(strings instanceof Array);
assert(strings.length === 3);
assert(strings[0] === "foo");
assert(strings[1] === "baz");
assert(strings[2] === "");
assert(p1 === 42);
assert(p2 === "qux");
return (strings, value) => `${value}${strings[0]}`;
}
var bar = 42;
assert(test5`foo${bar}baz${"qux"}``test${123}` === "123test");
function review(strings, name, rating) {
return `${strings[0]}**${name}**${strings[1]}_${rating}_${strings[2]}`;
}
var name = "SerenityOS";
var rating = "great";
assert(review`${name} is a ${rating} project!` === "**SerenityOS** is a _great_ project!");
console.log("PASS");
} catch (e) {
console.log("FAIL: " + e);
}