mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-28 17:14:52 +01:00
In summary - some of the changes here are focused on producing the same test results for SemanticsIR, but I think the next step will be to change the SemanticsIR structure to reduce how much is added to the traversal stack.
Switching semantics to a postorder traversal is intended to be more efficient. The traversal stack is to eliminate risk of recursion limits within the semantic analysis that could come from layered code structures. However, we need to start considering the implications for type-checking and what the ParseTree looks like, as well as copying of data here.
As we start thinking about type-checking in SemanticsIR, it's helpful for a function to know its own signature in order to perform lookup recursive calls. The challenge in the post-order walk without this change is it doesn't know it's in a function definition (or similar) until it reaches the FunctionDeclaration; this restructures so that either:
1. For a declaration, the signature is a child of FunctionDeclaration(";")
2. For a definition, the signature is a child of FunctionDefinitionStart("{") which pairs with FunctionDefinition("}"), replacing CodeBlock.
This similarly reorients CodeBlock to be CodeBlockStart("{") as the first child of CodeBlock("}"). I'm not doing that with ParameterList here just because it affects a bit more, and felt like it could be delayed.
Overall, my goal is making the postorder traversal more intuitive along scope boundaries. I think we may also not need subtree_size, so I'm avoiding use of that now.
Currently the SemanticsIRFactory implementation is less clean than I might like (there are a couple comments to this point), but I was starting to feel like a more complete rewrite would be appropriate rather than trying to clean it up further: in particular, I think the node structures are off, but changing them is significant and also changes test output; in turn it may also warrant more substantial ParseTree changes. If you prefer from a reviewer POV, I can do a more complete rewrite.
257 lines
9.6 KiB
C++
257 lines
9.6 KiB
C++
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
|
// Exceptions. See /LICENSE for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
#include "toolchain/parser/parse_tree.h"
|
|
|
|
#include <gmock/gmock.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <forward_list>
|
|
|
|
#include "llvm/Support/FormatVariadic.h"
|
|
#include "toolchain/common/yaml_test_helpers.h"
|
|
#include "toolchain/diagnostics/diagnostic_emitter.h"
|
|
#include "toolchain/diagnostics/mocks.h"
|
|
#include "toolchain/lexer/tokenized_buffer.h"
|
|
|
|
namespace Carbon::Testing {
|
|
namespace {
|
|
|
|
using ::testing::AtLeast;
|
|
using ::testing::ElementsAre;
|
|
using ::testing::Eq;
|
|
|
|
class ParseTreeTest : public ::testing::Test {
|
|
protected:
|
|
auto GetSourceBuffer(llvm::Twine t) -> SourceBuffer& {
|
|
source_storage.push_front(
|
|
std::move(*SourceBuffer::CreateFromText(t.str())));
|
|
return source_storage.front();
|
|
}
|
|
|
|
auto GetTokenizedBuffer(llvm::Twine t) -> TokenizedBuffer& {
|
|
token_storage.push_front(
|
|
TokenizedBuffer::Lex(GetSourceBuffer(t), consumer));
|
|
return token_storage.front();
|
|
}
|
|
|
|
std::forward_list<SourceBuffer> source_storage;
|
|
std::forward_list<TokenizedBuffer> token_storage;
|
|
DiagnosticConsumer& consumer = ConsoleDiagnosticConsumer();
|
|
};
|
|
|
|
TEST_F(ParseTreeTest, DefaultInvalid) {
|
|
ParseTree::Node node;
|
|
EXPECT_FALSE(node.is_valid());
|
|
}
|
|
|
|
TEST_F(ParseTreeTest, IsValid) {
|
|
TokenizedBuffer tokens = GetTokenizedBuffer("");
|
|
ParseTree tree = ParseTree::Parse(tokens, consumer);
|
|
EXPECT_TRUE((*tree.postorder().begin()).is_valid());
|
|
}
|
|
|
|
TEST_F(ParseTreeTest, OperatorWhitespaceErrors) {
|
|
// Test dispositions: Recovered means we issued an error but recovered a
|
|
// proper parse tree; Failed means we didn't fully recover from the error.
|
|
enum Kind { Valid, Recovered, Failed };
|
|
|
|
struct Testcase {
|
|
const char* input;
|
|
Kind kind;
|
|
} testcases[] = {
|
|
{"var v: Type = i8*;", Valid},
|
|
{"var v: Type = i8 *;", Recovered},
|
|
{"var v: Type = i8* ;", Valid},
|
|
{"var v: Type = i8 * ;", Recovered},
|
|
{"var n: i8 = n * n;", Valid},
|
|
{"var n: i8 = n*n;", Valid},
|
|
{"var n: i8 = (n)*3;", Valid},
|
|
{"var n: i8 = 3*(n);", Valid},
|
|
{"var n: i8 = n *n;", Recovered},
|
|
// TODO: We could figure out that this first Failed example is infix
|
|
// with one-token lookahead.
|
|
{"var n: i8 = n* n;", Failed},
|
|
{"var n: i8 = n* -n;", Failed},
|
|
{"var n: i8 = n* *p;", Failed},
|
|
// TODO: We try to form (n*)*p and reject due to missing parentheses
|
|
// before we notice the missing whitespace around the second `*`.
|
|
// It'd be better to (somehow) form n*(*p) and reject due to the missing
|
|
// whitespace around the first `*`.
|
|
{"var n: i8 = n**p;", Failed},
|
|
{"var n: i8 = -n;", Valid},
|
|
{"var n: i8 = - n;", Recovered},
|
|
{"var n: i8 =-n;", Valid},
|
|
{"var n: i8 =- n;", Recovered},
|
|
{"var n: i8 = F(i8 *);", Recovered},
|
|
{"var n: i8 = F(i8 *, 0);", Recovered},
|
|
};
|
|
|
|
for (auto [input, kind] : testcases) {
|
|
TokenizedBuffer tokens = GetTokenizedBuffer(input);
|
|
ErrorTrackingDiagnosticConsumer error_tracker(consumer);
|
|
ParseTree tree = ParseTree::Parse(tokens, error_tracker);
|
|
EXPECT_THAT(tree.has_errors(), Eq(kind == Failed)) << input;
|
|
EXPECT_THAT(error_tracker.seen_error(), Eq(kind != Valid)) << input;
|
|
}
|
|
}
|
|
|
|
TEST_F(ParseTreeTest, StructErrors) {
|
|
struct Testcase {
|
|
llvm::StringLiteral input;
|
|
::testing::Matcher<const Diagnostic&> diag_matcher;
|
|
};
|
|
Testcase testcases[] = {
|
|
{"var x: {i32} = {};",
|
|
IsDiagnosticMessage("Expected `.field: type` or `.field = value`.")},
|
|
{"var x: {a} = {};",
|
|
IsDiagnosticMessage("Expected `.field: type` or `.field = value`.")},
|
|
{"var x: {a:} = {};",
|
|
IsDiagnosticMessage("Expected `.field: type` or `.field = value`.")},
|
|
{"var x: {a=} = {};",
|
|
IsDiagnosticMessage("Expected `.field: type` or `.field = value`.")},
|
|
{"var x: {.} = {};",
|
|
IsDiagnosticMessage("Expected identifier after `.`.")},
|
|
{"var x: {.\"hello\" = 0, .y = 4} = {};",
|
|
IsDiagnosticMessage("Expected identifier after `.`.")},
|
|
{"var x: {.\"hello\": i32, .y: i32} = {};",
|
|
IsDiagnosticMessage("Expected identifier after `.`.")},
|
|
{"var x: {.a} = {};",
|
|
IsDiagnosticMessage("Expected `.field: type` or `.field = value`.")},
|
|
{"var x: {.a:} = {};", IsDiagnosticMessage("Expected expression.")},
|
|
{"var x: {.a=} = {};", IsDiagnosticMessage("Expected expression.")},
|
|
{"var x: {.a: i32, .b = 0} = {};",
|
|
IsDiagnosticMessage("Expected `.field: type`.")},
|
|
{"var x: {.a = 0, b: i32} = {};",
|
|
IsDiagnosticMessage("Expected `.field = value`.")},
|
|
{"var x: {,} = {};",
|
|
IsDiagnosticMessage("Expected `.field: type` or `.field = value`.")},
|
|
{"var x: {.a: i32,,} = {};",
|
|
IsDiagnosticMessage("Expected `.field: type`.")},
|
|
{"var x: {.a = 0,,} = {};",
|
|
IsDiagnosticMessage("Expected `.field = value`.")},
|
|
{"var x: {.a: i32 banana} = {.a = 0};",
|
|
IsDiagnosticMessage("Expected `,` or `}`.")},
|
|
{"var x: {.a: i32} = {.a = 0 banana};",
|
|
IsDiagnosticMessage("Expected `,` or `}`.")},
|
|
};
|
|
|
|
for (const Testcase& testcase : testcases) {
|
|
TokenizedBuffer tokens = GetTokenizedBuffer(testcase.input);
|
|
Testing::MockDiagnosticConsumer consumer;
|
|
EXPECT_CALL(consumer, HandleDiagnostic(testcase.diag_matcher));
|
|
ParseTree tree = ParseTree::Parse(tokens, consumer);
|
|
EXPECT_TRUE(tree.has_errors());
|
|
}
|
|
}
|
|
|
|
TEST_F(ParseTreeTest, PrintingAsYAML) {
|
|
TokenizedBuffer tokens = GetTokenizedBuffer("fn F();");
|
|
ParseTree tree = ParseTree::Parse(tokens, consumer);
|
|
EXPECT_FALSE(tree.has_errors());
|
|
std::string print_output;
|
|
llvm::raw_string_ostream print_stream(print_output);
|
|
tree.Print(print_stream);
|
|
print_stream.flush();
|
|
|
|
auto parameter_list = Yaml::SequenceValue{
|
|
Yaml::MappingValue{
|
|
{"node_index", "2"}, {"kind", "ParameterListEnd"}, {"text", ")"}},
|
|
};
|
|
|
|
auto function_decl = Yaml::SequenceValue{
|
|
Yaml::MappingValue{
|
|
{"node_index", "0"}, {"kind", "FunctionIntroducer"}, {"text", "fn"}},
|
|
Yaml::MappingValue{
|
|
{"node_index", "1"}, {"kind", "DeclaredName"}, {"text", "F"}},
|
|
Yaml::MappingValue{{"node_index", "3"},
|
|
{"kind", "ParameterList"},
|
|
{"text", "("},
|
|
{"subtree_size", "2"},
|
|
{"children", parameter_list}},
|
|
};
|
|
|
|
auto file = Yaml::SequenceValue{
|
|
Yaml::MappingValue{{"node_index", "4"},
|
|
{"kind", "FunctionDeclaration"},
|
|
{"text", ";"},
|
|
{"subtree_size", "5"},
|
|
{"children", function_decl}},
|
|
Yaml::MappingValue{
|
|
{"node_index", "5"}, {"kind", "FileEnd"}, {"text", ""}},
|
|
};
|
|
|
|
EXPECT_THAT(Yaml::Value::FromText(print_output), ElementsAre(file));
|
|
}
|
|
|
|
TEST_F(ParseTreeTest, RecursionLimit) {
|
|
std::string code = "fn Foo() { return ";
|
|
code.append(10000, '(');
|
|
code.append(10000, ')');
|
|
code += "; }";
|
|
TokenizedBuffer tokens = GetTokenizedBuffer(code);
|
|
ASSERT_FALSE(tokens.has_errors());
|
|
Testing::MockDiagnosticConsumer consumer;
|
|
// Recursion might be exceeded multiple times due to quirks in parse tree
|
|
// handling; we only need to be sure it's hit at least once for test
|
|
// correctness.
|
|
EXPECT_CALL(consumer, HandleDiagnostic(IsDiagnosticMessage(
|
|
llvm::formatv("Exceeded recursion limit ({0})",
|
|
ParseTree::StackDepthLimit)
|
|
.str())))
|
|
.Times(AtLeast(1));
|
|
ParseTree tree = ParseTree::Parse(tokens, consumer);
|
|
EXPECT_TRUE(tree.has_errors());
|
|
}
|
|
|
|
TEST_F(ParseTreeTest, ParsePostfixExpressionRegression) {
|
|
// Stack depth errors could cause ParsePostfixExpression to infinitely loop
|
|
// when calling children and those children error. Because of the fragility of
|
|
// stack depth, this tries a few different values.
|
|
for (int n = 0; n <= 10; ++n) {
|
|
std::string code = "var x: auto = ";
|
|
code.append(ParseTree::StackDepthLimit - n, '*');
|
|
code += "(z);";
|
|
TokenizedBuffer tokens = GetTokenizedBuffer(code);
|
|
ASSERT_FALSE(tokens.has_errors());
|
|
ParseTree tree = ParseTree::Parse(tokens, consumer);
|
|
EXPECT_TRUE(tree.has_errors());
|
|
}
|
|
}
|
|
|
|
TEST_F(ParseTreeTest, PackageErrors) {
|
|
struct TestCase {
|
|
llvm::StringLiteral input;
|
|
::testing::Matcher<const Diagnostic&> diag_matcher;
|
|
};
|
|
|
|
TestCase testcases[] = {
|
|
{"package;", IsDiagnosticMessage("Expected identifier after `package`.")},
|
|
{"package fn;",
|
|
IsDiagnosticMessage("Expected identifier after `package`.")},
|
|
{"package library \"Shapes\" api;",
|
|
IsDiagnosticMessage("Expected identifier after `package`.")},
|
|
{"package Geometry library Shapes api;",
|
|
IsDiagnosticMessage(
|
|
"Expected a string literal to specify the library name.")},
|
|
{"package Geometry \"Shapes\" api;",
|
|
IsDiagnosticMessage("Missing `library` keyword.")},
|
|
{"package Geometry api",
|
|
IsDiagnosticMessage("Expected `;` to end package directive.")},
|
|
{"package Geometry;", IsDiagnosticMessage("Expected a `api` or `impl`.")},
|
|
{R"(package Foo library "bar" "baz";)",
|
|
IsDiagnosticMessage("Expected a `api` or `impl`.")}};
|
|
|
|
for (const TestCase& testcase : testcases) {
|
|
TokenizedBuffer tokens = GetTokenizedBuffer(testcase.input);
|
|
Testing::MockDiagnosticConsumer consumer;
|
|
EXPECT_CALL(consumer, HandleDiagnostic(testcase.diag_matcher));
|
|
ParseTree tree = ParseTree::Parse(tokens, consumer);
|
|
EXPECT_TRUE(tree.has_errors());
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace Carbon::Testing
|