aboutsummaryrefslogtreecommitdiff
path: root/backend
diff options
context:
space:
mode:
Diffstat (limited to 'backend')
-rw-r--r--backend/include/rcc/Support/TextDiagnostic.h83
-rw-r--r--backend/include/rcc/Support/YAML.h197
-rw-r--r--backend/lib/Support/CMakeLists.txt12
-rw-r--r--backend/lib/Support/TextDiagnostic.cpp91
-rw-r--r--backend/lib/Support/YAML.cpp1217
-rw-r--r--backend/unittests/Support/CMakeLists.txt20
-rw-r--r--backend/unittests/Support/TextDiagnosticTest.cpp43
-rw-r--r--backend/unittests/Support/YAMLTest.cpp232
8 files changed, 1890 insertions, 5 deletions
diff --git a/backend/include/rcc/Support/TextDiagnostic.h b/backend/include/rcc/Support/TextDiagnostic.h
new file mode 100644
index 0000000..4c21486
--- /dev/null
+++ b/backend/include/rcc/Support/TextDiagnostic.h
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef RCC_SUPPORT_TEXTDIAGNOSTIC_H
+#define RCC_SUPPORT_TEXTDIAGNOSTIC_H
+
+#include <cstddef>
+#include <span>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace rcc {
+
+struct TextPosition final {
+ std::size_t Offset = 0;
+ std::size_t Line = 1;
+ std::size_t Column = 1;
+
+ friend constexpr bool operator==(TextPosition Left, TextPosition Right) noexcept = default;
+};
+
+struct TextRange final {
+ TextPosition Begin;
+ TextPosition End;
+
+ [[nodiscard]] constexpr bool isOrdered() const noexcept {
+ return Begin.Offset <= End.Offset;
+ }
+
+ friend constexpr bool operator==(TextRange Left, TextRange Right) noexcept = default;
+};
+
+enum class DiagnosticSeverity : unsigned char {
+ Note,
+ Warning,
+ Error,
+};
+
+[[nodiscard]] std::string_view getDiagnosticSeverityName(DiagnosticSeverity Severity) noexcept;
+
+struct TextDiagnostic final {
+ DiagnosticSeverity Severity = DiagnosticSeverity::Error;
+ std::string Category;
+ std::string Message;
+ std::string SourceName;
+ TextRange Range;
+};
+
+class TextSource final {
+public:
+ TextSource(std::string Name, std::string Text);
+
+ [[nodiscard]] std::string_view name() const noexcept {
+ return Name;
+ }
+
+ [[nodiscard]] std::string_view text() const noexcept {
+ return Text;
+ }
+
+ [[nodiscard]] std::size_t lineCount() const noexcept {
+ return LineOffsets.size();
+ }
+
+ [[nodiscard]] TextPosition positionAt(std::size_t Offset) const noexcept;
+ [[nodiscard]] TextRange range(std::size_t Begin, std::size_t End) const noexcept;
+ [[nodiscard]] std::string_view line(std::size_t OneBasedLine) const noexcept;
+ [[nodiscard]] std::span<const std::size_t> lineOffsets() const noexcept {
+ return LineOffsets;
+ }
+
+private:
+ std::string Name;
+ std::string Text;
+ std::vector<std::size_t> LineOffsets;
+};
+
+[[nodiscard]] std::string formatTextDiagnostic(const TextDiagnostic &Diagnostic,
+ const TextSource &Source);
+
+} // namespace rcc
+
+#endif // RCC_SUPPORT_TEXTDIAGNOSTIC_H
diff --git a/backend/include/rcc/Support/YAML.h b/backend/include/rcc/Support/YAML.h
new file mode 100644
index 0000000..2e403a9
--- /dev/null
+++ b/backend/include/rcc/Support/YAML.h
@@ -0,0 +1,197 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef RCC_SUPPORT_YAML_H
+#define RCC_SUPPORT_YAML_H
+
+#include "rcc/Support/TextDiagnostic.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <limits>
+#include <optional>
+#include <span>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace rcc::yaml {
+
+class Parser;
+
+class NodeID final {
+public:
+ constexpr NodeID() noexcept = default;
+
+ [[nodiscard]] constexpr bool isValid() const noexcept {
+ return Value != InvalidValue;
+ }
+
+ [[nodiscard]] constexpr std::uint32_t rawValue() const noexcept {
+ return Value;
+ }
+
+ friend constexpr bool operator==(NodeID Left, NodeID Right) noexcept = default;
+
+private:
+ static constexpr std::uint32_t InvalidValue = std::numeric_limits<std::uint32_t>::max();
+
+ explicit constexpr NodeID(std::uint32_t Value) noexcept : Value(Value) {}
+
+ std::uint32_t Value = InvalidValue;
+
+ friend class Parser;
+ friend class Stream;
+};
+
+enum class NodeKind : std::uint8_t {
+ Null,
+ Scalar,
+ Sequence,
+ Mapping,
+};
+
+enum class ScalarStyle : std::uint8_t {
+ Plain,
+ SingleQuoted,
+ DoubleQuoted,
+ Literal,
+ Folded,
+};
+
+enum class CollectionStyle : std::uint8_t {
+ Block,
+ Flow,
+};
+
+struct MappingEntry final {
+ std::string Key;
+ TextRange KeyRange;
+ NodeID Value;
+};
+
+class Node final {
+public:
+ [[nodiscard]] NodeKind kind() const noexcept {
+ return Kind;
+ }
+
+ [[nodiscard]] const TextRange &range() const noexcept {
+ return Range;
+ }
+
+ [[nodiscard]] ScalarStyle scalarStyle() const noexcept {
+ return ScalarStyleValue;
+ }
+
+ [[nodiscard]] CollectionStyle collectionStyle() const noexcept {
+ return CollectionStyleValue;
+ }
+
+ [[nodiscard]] std::string_view scalar() const noexcept {
+ return Scalar;
+ }
+
+ [[nodiscard]] std::span<const NodeID> sequence() const noexcept {
+ return Sequence;
+ }
+
+ [[nodiscard]] std::span<const MappingEntry> mapping() const noexcept {
+ return Mapping;
+ }
+
+ [[nodiscard]] std::optional<NodeID> mappingValue(std::string_view Key) const noexcept;
+
+private:
+ explicit Node(NodeKind Kind, TextRange Range) : Kind(Kind), Range(Range) {}
+
+ NodeKind Kind;
+ TextRange Range;
+ ScalarStyle ScalarStyleValue = ScalarStyle::Plain;
+ CollectionStyle CollectionStyleValue = CollectionStyle::Block;
+ std::string Scalar;
+ std::vector<NodeID> Sequence;
+ std::vector<MappingEntry> Mapping;
+
+ friend class Parser;
+};
+
+struct Document final {
+ NodeID Root;
+ TextRange Range;
+ bool HasExplicitStart = false;
+ bool HasExplicitEnd = false;
+};
+
+class Stream final {
+public:
+ Stream(const Stream &) = delete;
+ Stream &operator=(const Stream &) = delete;
+ Stream(Stream &&) noexcept = default;
+ Stream &operator=(Stream &&) noexcept = default;
+ ~Stream() = default;
+
+ [[nodiscard]] const TextSource &source() const noexcept {
+ return Source;
+ }
+
+ [[nodiscard]] std::span<const Document> documents() const noexcept {
+ return Documents;
+ }
+
+ [[nodiscard]] const Node *node(NodeID ID) const noexcept;
+ [[nodiscard]] std::size_t nodeCount() const noexcept {
+ return Nodes.size();
+ }
+
+private:
+ Stream(std::string SourceName, std::string SourceText)
+ : Source(std::move(SourceName), std::move(SourceText)) {}
+
+ TextSource Source;
+ std::vector<Node> Nodes;
+ std::vector<Document> Documents;
+
+ friend class Parser;
+};
+
+struct ParseOptions final {
+ std::size_t MaximumDepth = 128;
+ std::size_t MaximumNodes = 1U << 20U;
+};
+
+class ParseResult final {
+public:
+ ParseResult(const ParseResult &) = delete;
+ ParseResult &operator=(const ParseResult &) = delete;
+ ParseResult(ParseResult &&) noexcept = default;
+ ParseResult &operator=(ParseResult &&) noexcept = default;
+ ~ParseResult() = default;
+
+ [[nodiscard]] bool succeeded() const noexcept {
+ return ParsedStream.has_value() && Diagnostics.empty();
+ }
+
+ [[nodiscard]] const Stream *stream() const noexcept {
+ return ParsedStream.has_value() ? ParsedStream.operator->() : nullptr;
+ }
+
+ [[nodiscard]] std::span<const TextDiagnostic> diagnostics() const noexcept {
+ return Diagnostics;
+ }
+
+private:
+ ParseResult() = default;
+
+ std::optional<Stream> ParsedStream;
+ std::vector<TextDiagnostic> Diagnostics;
+
+ friend class Parser;
+};
+
+[[nodiscard]] ParseResult parse(std::string SourceName, std::string SourceText,
+ ParseOptions Options = {});
+
+} // namespace rcc::yaml
+
+#endif // RCC_SUPPORT_YAML_H
diff --git a/backend/lib/Support/CMakeLists.txt b/backend/lib/Support/CMakeLists.txt
index 88c959d..ee85084 100644
--- a/backend/lib/Support/CMakeLists.txt
+++ b/backend/lib/Support/CMakeLists.txt
@@ -1,9 +1,11 @@
# SPDX-License-Identifier: GPL-3.0-or-later
-set(RCCBackendSupportSources Error.cpp)
+set(RCCBackendSupportSources Error.cpp TextDiagnostic.cpp YAML.cpp)
set(
RCCBackendSupportHeaders
"${RCC_BACKEND_SOURCE_DIR}/include/rcc/Support/Error.h"
+ "${RCC_BACKEND_SOURCE_DIR}/include/rcc/Support/TextDiagnostic.h"
+ "${RCC_BACKEND_SOURCE_DIR}/include/rcc/Support/YAML.h"
)
add_library(RCCBackendSupport ${RCCBackendSupportSources})
@@ -35,6 +37,12 @@ install(
rcc_backend_register_format_files(
"${CMAKE_CURRENT_SOURCE_DIR}/Error.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/TextDiagnostic.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/YAML.cpp"
${RCCBackendSupportHeaders}
)
-rcc_backend_register_tidy_files("${CMAKE_CURRENT_SOURCE_DIR}/Error.cpp")
+rcc_backend_register_tidy_files(
+ "${CMAKE_CURRENT_SOURCE_DIR}/Error.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/TextDiagnostic.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/YAML.cpp"
+)
diff --git a/backend/lib/Support/TextDiagnostic.cpp b/backend/lib/Support/TextDiagnostic.cpp
new file mode 100644
index 0000000..1c027c3
--- /dev/null
+++ b/backend/lib/Support/TextDiagnostic.cpp
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/Support/TextDiagnostic.h"
+
+#include <algorithm>
+#include <iterator>
+#include <sstream>
+#include <utility>
+
+namespace rcc {
+
+std::string_view getDiagnosticSeverityName(DiagnosticSeverity Severity) noexcept {
+ switch (Severity) {
+ case DiagnosticSeverity::Note:
+ return "note";
+ case DiagnosticSeverity::Warning:
+ return "warning";
+ case DiagnosticSeverity::Error:
+ return "error";
+ }
+
+ return "unknown";
+}
+
+TextSource::TextSource(std::string Name, std::string Text)
+ : Name(std::move(Name)), Text(std::move(Text)), LineOffsets{0} {
+ for (std::size_t Index = 0; Index < this->Text.size(); ++Index) {
+ if (this->Text[Index] == '\n')
+ LineOffsets.push_back(Index + 1);
+ }
+}
+
+TextPosition TextSource::positionAt(std::size_t Offset) const noexcept {
+ Offset = std::min(Offset, Text.size());
+ const auto LineIterator = std::upper_bound(LineOffsets.begin(), LineOffsets.end(), Offset);
+ const std::size_t LineIndex =
+ static_cast<std::size_t>(std::distance(LineOffsets.begin(), LineIterator) - 1);
+ return TextPosition{
+ .Offset = Offset,
+ .Line = LineIndex + 1,
+ .Column = Offset - LineOffsets[LineIndex] + 1,
+ };
+}
+
+TextRange TextSource::range(std::size_t Begin, std::size_t End) const noexcept {
+ Begin = std::min(Begin, Text.size());
+ End = std::clamp(End, Begin, Text.size());
+ return TextRange{.Begin = positionAt(Begin), .End = positionAt(End)};
+}
+
+std::string_view TextSource::line(std::size_t OneBasedLine) const noexcept {
+ if (OneBasedLine == 0 || OneBasedLine > LineOffsets.size())
+ return {};
+
+ const std::size_t Begin = LineOffsets[OneBasedLine - 1];
+ std::size_t End = OneBasedLine < LineOffsets.size() ? LineOffsets[OneBasedLine] - 1 : Text.size();
+ if (End > Begin && Text[End - 1] == '\r')
+ --End;
+ return std::string_view(Text).substr(Begin, End - Begin);
+}
+
+std::string formatTextDiagnostic(const TextDiagnostic &Diagnostic, const TextSource &Source) {
+ std::ostringstream Output;
+ const TextPosition Position = Diagnostic.Range.Begin;
+ const std::string_view SourceName =
+ Diagnostic.SourceName.empty() ? Source.name() : std::string_view(Diagnostic.SourceName);
+
+ Output << SourceName << ':' << Position.Line << ':' << Position.Column << ": "
+ << getDiagnosticSeverityName(Diagnostic.Severity);
+ if (!Diagnostic.Category.empty())
+ Output << '[' << Diagnostic.Category << ']';
+ Output << ": " << Diagnostic.Message;
+
+ const std::string_view SourceLine = Source.line(Position.Line);
+ if (SourceLine.empty())
+ return Output.str();
+
+ Output << '\n' << SourceLine << '\n';
+ const std::size_t PrefixWidth = Position.Column > 0 ? Position.Column - 1 : 0;
+ for (std::size_t Index = 0; Index < PrefixWidth && Index < SourceLine.size(); ++Index)
+ Output << (SourceLine[Index] == '\t' ? '\t' : ' ');
+
+ Output << '^';
+ if (Diagnostic.Range.End.Line == Position.Line &&
+ Diagnostic.Range.End.Column > Position.Column + 1) {
+ Output << std::string(Diagnostic.Range.End.Column - Position.Column - 1, '~');
+ }
+ return Output.str();
+}
+
+} // namespace rcc
diff --git a/backend/lib/Support/YAML.cpp b/backend/lib/Support/YAML.cpp
new file mode 100644
index 0000000..1d3ed2f
--- /dev/null
+++ b/backend/lib/Support/YAML.cpp
@@ -0,0 +1,1217 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/Support/YAML.h"
+
+#include <algorithm>
+#include <limits>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <utility>
+
+namespace rcc::yaml {
+namespace {
+
+struct PhysicalLine final {
+ std::size_t Begin = 0;
+ std::size_t ContentEnd = 0;
+ std::size_t End = 0;
+ std::size_t Indent = 0;
+ bool HasIndentTab = false;
+ bool IsBlank = true;
+};
+
+enum class DocumentMarker : std::uint8_t {
+ None,
+ Start,
+ End,
+};
+
+[[nodiscard]] bool isAsciiSpace(char Character) noexcept {
+ return Character == ' ' || Character == '\t';
+}
+
+[[nodiscard]] bool isHexDigit(char Character) noexcept {
+ return (Character >= '0' && Character <= '9') || (Character >= 'a' && Character <= 'f') ||
+ (Character >= 'A' && Character <= 'F');
+}
+
+[[nodiscard]] unsigned hexValue(char Character) noexcept {
+ if (Character >= '0' && Character <= '9')
+ return static_cast<unsigned>(Character - '0');
+ if (Character >= 'a' && Character <= 'f')
+ return static_cast<unsigned>(Character - 'a') + 10U;
+ return static_cast<unsigned>(Character - 'A') + 10U;
+}
+
+[[nodiscard]] bool isNullSpelling(std::string_view Value) noexcept {
+ return Value == "~" || Value == "null" || Value == "Null" || Value == "NULL";
+}
+
+void appendUTF8(std::string &Output, std::uint32_t CodePoint) {
+ if (CodePoint <= 0x7FU) {
+ Output.push_back(static_cast<char>(CodePoint));
+ return;
+ }
+ if (CodePoint <= 0x7FFU) {
+ Output.push_back(static_cast<char>(0xC0U | (CodePoint >> 6U)));
+ Output.push_back(static_cast<char>(0x80U | (CodePoint & 0x3FU)));
+ return;
+ }
+ if (CodePoint <= 0xFFFFU) {
+ Output.push_back(static_cast<char>(0xE0U | (CodePoint >> 12U)));
+ Output.push_back(static_cast<char>(0x80U | ((CodePoint >> 6U) & 0x3FU)));
+ Output.push_back(static_cast<char>(0x80U | (CodePoint & 0x3FU)));
+ return;
+ }
+ Output.push_back(static_cast<char>(0xF0U | (CodePoint >> 18U)));
+ Output.push_back(static_cast<char>(0x80U | ((CodePoint >> 12U) & 0x3FU)));
+ Output.push_back(static_cast<char>(0x80U | ((CodePoint >> 6U) & 0x3FU)));
+ Output.push_back(static_cast<char>(0x80U | (CodePoint & 0x3FU)));
+}
+
+} // namespace
+
+std::optional<NodeID> Node::mappingValue(std::string_view Key) const noexcept {
+ if (Kind != NodeKind::Mapping)
+ return std::nullopt;
+ const auto Iterator = std::find_if(Mapping.begin(), Mapping.end(),
+ [Key](const MappingEntry &Entry) { return Entry.Key == Key; });
+ if (Iterator == Mapping.end())
+ return std::nullopt;
+ return Iterator->Value;
+}
+
+const Node *Stream::node(NodeID ID) const noexcept {
+ if (!ID.isValid() || ID.rawValue() >= Nodes.size())
+ return nullptr;
+ return &Nodes[ID.rawValue()];
+}
+
+class Parser final {
+public:
+ Parser(std::string SourceName, std::string SourceText, ParseOptions Options)
+ : Parsed(std::move(SourceName), std::move(SourceText)), Options(Options) {
+ buildLineTable();
+ }
+
+ [[nodiscard]] ParseResult run() {
+ if (Options.MaximumDepth == 0) {
+ diagnose(0, 0, "maximum YAML nesting depth must be greater than zero");
+ return failureResult();
+ }
+ if (Options.MaximumNodes == 0 ||
+ Options.MaximumNodes >= std::numeric_limits<std::uint32_t>::max()) {
+ diagnose(0, 0, "maximum YAML node count is outside the supported range");
+ return failureResult();
+ }
+ if (Parsed.Source.text().find('\0') != std::string_view::npos) {
+ const std::size_t Offset = Parsed.Source.text().find('\0');
+ diagnose(Offset, Offset + 1, "NUL bytes are not allowed in YAML input");
+ return failureResult();
+ }
+
+ std::size_t LineIndex = 0;
+ skipBlankLines(LineIndex, Lines.size());
+ while (LineIndex < Lines.size()) {
+ if (Lines[LineIndex].Indent == 0 && Parsed.Source.text()[Lines[LineIndex].Begin] == '%') {
+ diagnose(Lines[LineIndex].Begin, Lines[LineIndex].ContentEnd,
+ "YAML directives are not supported");
+ return failureResult();
+ }
+ if (markerAt(LineIndex) == DocumentMarker::End) {
+ diagnose(Lines[LineIndex].Begin, Lines[LineIndex].ContentEnd,
+ "document end marker has no open document");
+ return failureResult();
+ }
+
+ const std::size_t DocumentStartLine = LineIndex;
+ const bool HasExplicitStart = markerAt(LineIndex) == DocumentMarker::Start;
+ std::size_t InlineStart = Lines[LineIndex].ContentEnd;
+ if (HasExplicitStart)
+ InlineStart = markerRemainder(LineIndex, 3);
+
+ const std::size_t SearchStart = HasExplicitStart ? LineIndex + 1 : LineIndex;
+ const std::size_t LimitLine = findDocumentBoundary(SearchStart);
+ NodeID Root;
+ if (HasExplicitStart && InlineStart < Lines[LineIndex].ContentEnd &&
+ Parsed.Source.text()[InlineStart] != '#') {
+ if (Parsed.Source.text()[InlineStart] == '|' || Parsed.Source.text()[InlineStart] == '>') {
+ Root = parseBlockScalar(LineIndex, InlineStart, 0, 1, LimitLine);
+ } else {
+ Root = parseInlineValueOnLine(LineIndex, InlineStart, 1, LimitLine);
+ }
+ } else {
+ if (HasExplicitStart)
+ ++LineIndex;
+ skipBlankLines(LineIndex, LimitLine);
+ if (LineIndex == LimitLine) {
+ const std::size_t Offset = HasExplicitStart ? Lines[DocumentStartLine].ContentEnd
+ : Lines[DocumentStartLine].Begin;
+ Root = addNullNode(Offset, Offset);
+ } else {
+ Root = parseBlockNode(LineIndex, Lines[LineIndex].Indent, 1, LimitLine);
+ }
+ }
+
+ if (Failed)
+ return failureResult();
+
+ skipBlankLines(LineIndex, LimitLine);
+ if (LineIndex != LimitLine) {
+ diagnose(Lines[LineIndex].Begin + Lines[LineIndex].Indent, Lines[LineIndex].ContentEnd,
+ "unexpected content after YAML document root");
+ return failureResult();
+ }
+
+ bool HasExplicitEnd = false;
+ if (LimitLine < Lines.size() && markerAt(LimitLine) == DocumentMarker::End) {
+ const std::size_t EndRemainder = markerRemainder(LimitLine, 3);
+ if (EndRemainder < Lines[LimitLine].ContentEnd &&
+ Parsed.Source.text()[EndRemainder] != '#') {
+ diagnose(EndRemainder, Lines[LimitLine].ContentEnd,
+ "document end marker may only be followed by a comment");
+ return failureResult();
+ }
+ HasExplicitEnd = true;
+ LineIndex = LimitLine + 1;
+ } else {
+ LineIndex = LimitLine;
+ }
+
+ const std::size_t DocumentBegin = Lines[DocumentStartLine].Begin;
+ const std::size_t DocumentEnd = HasExplicitEnd
+ ? Lines[LimitLine].ContentEnd
+ : (LimitLine == 0 ? 0 : Lines[LimitLine - 1].End);
+ Parsed.Documents.push_back(Document{
+ .Root = Root,
+ .Range = Parsed.Source.range(DocumentBegin, DocumentEnd),
+ .HasExplicitStart = HasExplicitStart,
+ .HasExplicitEnd = HasExplicitEnd,
+ });
+
+ skipBlankLines(LineIndex, Lines.size());
+ }
+
+ ParseResult Result;
+ Result.ParsedStream.emplace(std::move(Parsed));
+ return Result;
+ }
+
+private:
+ void buildLineTable() {
+ const std::string_view Text = Parsed.Source.text();
+ std::size_t Begin = 0;
+ while (Begin <= Text.size()) {
+ const std::size_t Newline = Text.find('\n', Begin);
+ const std::size_t End = Newline == std::string_view::npos ? Text.size() : Newline + 1;
+ std::size_t ContentEnd = Newline == std::string_view::npos ? Text.size() : Newline;
+ if (ContentEnd > Begin && Text[ContentEnd - 1] == '\r')
+ --ContentEnd;
+
+ PhysicalLine Line{
+ .Begin = Begin,
+ .ContentEnd = ContentEnd,
+ .End = End,
+ };
+ std::size_t Cursor = Begin;
+ while (Cursor < ContentEnd && Text[Cursor] == ' ')
+ ++Cursor;
+ Line.Indent = Cursor - Begin;
+ std::size_t ContentCursor = Cursor;
+ while (ContentCursor < ContentEnd && isAsciiSpace(Text[ContentCursor])) {
+ if (Text[ContentCursor] == '\t')
+ Line.HasIndentTab = true;
+ ++ContentCursor;
+ }
+ Line.IsBlank = ContentCursor == ContentEnd || Text[ContentCursor] == '#';
+ Lines.push_back(Line);
+
+ if (Newline == std::string_view::npos)
+ break;
+ Begin = End;
+ }
+ }
+
+ [[nodiscard]] ParseResult failureResult() {
+ ParseResult Result;
+ Result.Diagnostics = std::move(Diagnostics);
+ return Result;
+ }
+
+ void diagnose(std::size_t Begin, std::size_t End, std::string Message) {
+ if (Failed)
+ return;
+ Failed = true;
+ Diagnostics.push_back(TextDiagnostic{
+ .Severity = DiagnosticSeverity::Error,
+ .Category = "yaml",
+ .Message = std::move(Message),
+ .SourceName = std::string(Parsed.Source.name()),
+ .Range = Parsed.Source.range(Begin, End),
+ });
+ }
+
+ void skipBlankLines(std::size_t &LineIndex, std::size_t LimitLine) {
+ while (LineIndex < LimitLine && Lines[LineIndex].IsBlank)
+ ++LineIndex;
+ }
+
+ [[nodiscard]] DocumentMarker markerAt(std::size_t LineIndex) const noexcept {
+ if (LineIndex >= Lines.size())
+ return DocumentMarker::None;
+ const PhysicalLine &Line = Lines[LineIndex];
+ if (Line.Indent != 0 || Line.ContentEnd - Line.Begin < 3)
+ return DocumentMarker::None;
+
+ const std::string_view Text = Parsed.Source.text();
+ const std::string_view Prefix = Text.substr(Line.Begin, 3);
+ if (Prefix != "---" && Prefix != "...")
+ return DocumentMarker::None;
+ if (Line.Begin + 3 < Line.ContentEnd) {
+ const char Next = Text[Line.Begin + 3];
+ if (!isAsciiSpace(Next) && Next != '#')
+ return DocumentMarker::None;
+ }
+ return Prefix == "---" ? DocumentMarker::Start : DocumentMarker::End;
+ }
+
+ [[nodiscard]] std::size_t markerRemainder(std::size_t LineIndex,
+ std::size_t MarkerWidth) const noexcept {
+ const PhysicalLine &Line = Lines[LineIndex];
+ std::size_t Cursor = Line.Begin + MarkerWidth;
+ const std::string_view Text = Parsed.Source.text();
+ while (Cursor < Line.ContentEnd && isAsciiSpace(Text[Cursor]))
+ ++Cursor;
+ return Cursor;
+ }
+
+ [[nodiscard]] std::size_t findDocumentBoundary(std::size_t StartLine) const noexcept {
+ for (std::size_t LineIndex = StartLine; LineIndex < Lines.size(); ++LineIndex) {
+ if (markerAt(LineIndex) != DocumentMarker::None)
+ return LineIndex;
+ }
+ return Lines.size();
+ }
+
+ [[nodiscard]] bool validateIndent(std::size_t LineIndex) {
+ if (!Lines[LineIndex].HasIndentTab)
+ return true;
+ diagnose(Lines[LineIndex].Begin, Lines[LineIndex].Begin + Lines[LineIndex].Indent,
+ "tab characters are not allowed in YAML indentation");
+ return false;
+ }
+
+ [[nodiscard]] NodeID parseBlockNode(std::size_t &LineIndex, std::size_t Indent, std::size_t Depth,
+ std::size_t LimitLine) {
+ if (!checkDepth(Depth))
+ return {};
+ skipBlankLines(LineIndex, LimitLine);
+ if (LineIndex >= LimitLine) {
+ const std::size_t Offset =
+ LimitLine < Lines.size() ? Lines[LimitLine].Begin : Parsed.Source.text().size();
+ return addNullNode(Offset, Offset);
+ }
+ if (!validateIndent(LineIndex))
+ return {};
+ if (Lines[LineIndex].Indent != Indent) {
+ diagnose(Lines[LineIndex].Begin + Lines[LineIndex].Indent, Lines[LineIndex].ContentEnd,
+ "inconsistent YAML indentation");
+ return {};
+ }
+
+ const std::size_t ContentStart = Lines[LineIndex].Begin + Indent;
+ if (isSequenceIndicator(ContentStart, Lines[LineIndex].ContentEnd))
+ return parseBlockSequence(LineIndex, Indent, Depth, LimitLine);
+ if (findBlockMappingColon(ContentStart, Lines[LineIndex].ContentEnd))
+ return parseBlockMapping(LineIndex, Indent, Depth, LimitLine);
+
+ NodeID Result;
+ if (Parsed.Source.text()[ContentStart] == '|' || Parsed.Source.text()[ContentStart] == '>') {
+ Result = parseBlockScalar(LineIndex, ContentStart, Indent, Depth, LimitLine);
+ } else {
+ Result = parseInlineValueOnLine(LineIndex, ContentStart, Depth, LimitLine);
+ }
+ return Result;
+ }
+
+ [[nodiscard]] NodeID parseBlockMapping(std::size_t &LineIndex, std::size_t Indent,
+ std::size_t Depth, std::size_t LimitLine) {
+ const std::size_t Begin = Lines[LineIndex].Begin + Indent;
+ std::vector<MappingEntry> Entries;
+ while (LineIndex < LimitLine) {
+ skipBlankLines(LineIndex, LimitLine);
+ if (LineIndex >= LimitLine || Lines[LineIndex].Indent < Indent)
+ break;
+ if (!validateIndent(LineIndex))
+ return {};
+ if (Lines[LineIndex].Indent > Indent) {
+ diagnose(Lines[LineIndex].Begin + Lines[LineIndex].Indent, Lines[LineIndex].ContentEnd,
+ "unexpected indentation in YAML mapping");
+ return {};
+ }
+
+ const std::size_t KeyStart = Lines[LineIndex].Begin + Indent;
+ if (!findBlockMappingColon(KeyStart, Lines[LineIndex].ContentEnd))
+ break;
+ if (!parseMappingEntry(LineIndex, KeyStart, Indent, Depth, LimitLine, Entries))
+ return {};
+ }
+
+ const std::size_t End =
+ LineIndex == 0 ? Begin : std::max(Begin, Lines[LineIndex - 1].ContentEnd);
+ return addMappingNode(Begin, End, CollectionStyle::Block, std::move(Entries));
+ }
+
+ [[nodiscard]] NodeID parseInlineStartedMapping(std::size_t &LineIndex, std::size_t KeyStart,
+ std::size_t MappingIndent, std::size_t Depth,
+ std::size_t LimitLine) {
+ const std::size_t Begin = KeyStart;
+ std::vector<MappingEntry> Entries;
+ if (!parseMappingEntry(LineIndex, KeyStart, MappingIndent, Depth, LimitLine, Entries))
+ return {};
+
+ while (LineIndex < LimitLine) {
+ skipBlankLines(LineIndex, LimitLine);
+ if (LineIndex >= LimitLine || Lines[LineIndex].Indent < MappingIndent)
+ break;
+ if (!validateIndent(LineIndex))
+ return {};
+ if (Lines[LineIndex].Indent > MappingIndent) {
+ diagnose(Lines[LineIndex].Begin + Lines[LineIndex].Indent, Lines[LineIndex].ContentEnd,
+ "unexpected indentation in YAML mapping");
+ return {};
+ }
+ const std::size_t ContinuationKey = Lines[LineIndex].Begin + MappingIndent;
+ if (!findBlockMappingColon(ContinuationKey, Lines[LineIndex].ContentEnd))
+ break;
+ if (!parseMappingEntry(LineIndex, ContinuationKey, MappingIndent, Depth, LimitLine, Entries))
+ return {};
+ }
+
+ const std::size_t End =
+ LineIndex == 0 ? Begin : std::max(Begin, Lines[LineIndex - 1].ContentEnd);
+ return addMappingNode(Begin, End, CollectionStyle::Block, std::move(Entries));
+ }
+
+ [[nodiscard]] bool parseMappingEntry(std::size_t &LineIndex, std::size_t KeyStart,
+ std::size_t ParentIndent, std::size_t Depth,
+ std::size_t LimitLine, std::vector<MappingEntry> &Entries) {
+ const PhysicalLine &Line = Lines[LineIndex];
+ const std::optional<std::size_t> Colon = findBlockMappingColon(KeyStart, Line.ContentEnd);
+ if (!Colon) {
+ diagnose(KeyStart, Line.ContentEnd, "expected ':' after YAML mapping key");
+ return false;
+ }
+
+ std::string Key;
+ TextRange KeyRange;
+ if (!parseMappingKey(KeyStart, *Colon, Key, KeyRange))
+ return false;
+ if (std::any_of(Entries.begin(), Entries.end(),
+ [&Key](const MappingEntry &Entry) { return Entry.Key == Key; })) {
+ diagnose(KeyStart, *Colon, "duplicate YAML mapping key '" + Key + "'");
+ return false;
+ }
+
+ std::size_t ValueStart = *Colon + 1;
+ const std::string_view Text = Parsed.Source.text();
+ while (ValueStart < Line.ContentEnd && isAsciiSpace(Text[ValueStart]))
+ ++ValueStart;
+
+ NodeID Value;
+ if (ValueStart == Line.ContentEnd || Text[ValueStart] == '#') {
+ const std::size_t CurrentLine = LineIndex;
+ std::size_t NestedLine = LineIndex + 1;
+ skipBlankLines(NestedLine, LimitLine);
+ if (NestedLine < LimitLine && Lines[NestedLine].Indent > ParentIndent) {
+ LineIndex = NestedLine;
+ Value = parseBlockNode(LineIndex, Lines[NestedLine].Indent, Depth + 1, LimitLine);
+ } else {
+ Value = addNullNode(*Colon + 1, *Colon + 1);
+ LineIndex = CurrentLine + 1;
+ }
+ } else if (Text[ValueStart] == '|' || Text[ValueStart] == '>') {
+ Value = parseBlockScalar(LineIndex, ValueStart, ParentIndent, Depth + 1, LimitLine);
+ } else {
+ Value = parseInlineValueOnLine(LineIndex, ValueStart, Depth + 1, LimitLine);
+ }
+ if (Failed)
+ return false;
+
+ Entries.push_back(MappingEntry{
+ .Key = std::move(Key),
+ .KeyRange = KeyRange,
+ .Value = Value,
+ });
+ return true;
+ }
+
+ [[nodiscard]] NodeID parseBlockSequence(std::size_t &LineIndex, std::size_t Indent,
+ std::size_t Depth, std::size_t LimitLine) {
+ const std::size_t Begin = Lines[LineIndex].Begin + Indent;
+ std::vector<NodeID> Items;
+ const std::string_view Text = Parsed.Source.text();
+
+ while (LineIndex < LimitLine) {
+ skipBlankLines(LineIndex, LimitLine);
+ if (LineIndex >= LimitLine || Lines[LineIndex].Indent < Indent)
+ break;
+ if (!validateIndent(LineIndex))
+ return {};
+ if (Lines[LineIndex].Indent > Indent) {
+ diagnose(Lines[LineIndex].Begin + Lines[LineIndex].Indent, Lines[LineIndex].ContentEnd,
+ "unexpected indentation in YAML sequence");
+ return {};
+ }
+
+ const std::size_t Dash = Lines[LineIndex].Begin + Indent;
+ if (!isSequenceIndicator(Dash, Lines[LineIndex].ContentEnd))
+ break;
+
+ std::size_t ItemStart = Dash + 1;
+ while (ItemStart < Lines[LineIndex].ContentEnd && isAsciiSpace(Text[ItemStart]))
+ ++ItemStart;
+
+ NodeID Item;
+ if (ItemStart == Lines[LineIndex].ContentEnd || Text[ItemStart] == '#') {
+ const std::size_t CurrentLine = LineIndex;
+ std::size_t NestedLine = LineIndex + 1;
+ skipBlankLines(NestedLine, LimitLine);
+ if (NestedLine < LimitLine && Lines[NestedLine].Indent > Indent) {
+ LineIndex = NestedLine;
+ Item = parseBlockNode(LineIndex, Lines[NestedLine].Indent, Depth + 1, LimitLine);
+ } else {
+ Item = addNullNode(Dash + 1, Dash + 1);
+ LineIndex = CurrentLine + 1;
+ }
+ } else if (Text[ItemStart] == '|' || Text[ItemStart] == '>') {
+ Item = parseBlockScalar(LineIndex, ItemStart, Indent, Depth + 1, LimitLine);
+ } else if (findBlockMappingColon(ItemStart, Lines[LineIndex].ContentEnd)) {
+ const std::size_t MappingIndent = ItemStart - Lines[LineIndex].Begin;
+ Item = parseInlineStartedMapping(LineIndex, ItemStart, MappingIndent, Depth + 1, LimitLine);
+ } else {
+ Item = parseInlineValueOnLine(LineIndex, ItemStart, Depth + 1, LimitLine);
+ }
+ if (Failed)
+ return {};
+ Items.push_back(Item);
+ }
+
+ const std::size_t End =
+ LineIndex == 0 ? Begin : std::max(Begin, Lines[LineIndex - 1].ContentEnd);
+ return addSequenceNode(Begin, End, CollectionStyle::Block, std::move(Items));
+ }
+
+ [[nodiscard]] NodeID parseInlineValueOnLine(std::size_t &LineIndex, std::size_t Begin,
+ std::size_t Depth, std::size_t LimitLine) {
+ const std::string_view Text = Parsed.Source.text();
+ const std::size_t InitialLine = LineIndex;
+ const std::size_t LineEnd = Lines[InitialLine].ContentEnd;
+ std::size_t ValueStart = Begin;
+ while (ValueStart < LineEnd && isAsciiSpace(Text[ValueStart]))
+ ++ValueStart;
+
+ const bool CanSpanLines =
+ ValueStart < LineEnd && (Text[ValueStart] == '[' || Text[ValueStart] == '{' ||
+ Text[ValueStart] == '\'' || Text[ValueStart] == '"');
+ if (!CanSpanLines) {
+ NodeID Result = parseInlineValue(ValueStart, LineEnd, Depth);
+ ++LineIndex;
+ return Result;
+ }
+
+ const std::size_t DocumentEnd =
+ LimitLine < Lines.size() ? Lines[LimitLine].Begin : Parsed.Source.text().size();
+ std::size_t Cursor = ValueStart;
+ NodeID Result = parseFlowNode(Cursor, DocumentEnd, Depth, false);
+ if (Failed)
+ return {};
+
+ const std::size_t LastValueOffset = Cursor == 0 ? 0 : Cursor - 1;
+ const std::size_t LastLine = Parsed.Source.positionAt(LastValueOffset).Line - 1;
+ if (LastLine >= Lines.size()) {
+ diagnose(Cursor, Cursor, "internal YAML line tracking failure");
+ return {};
+ }
+
+ std::size_t Tail = Cursor;
+ while (Tail < Lines[LastLine].ContentEnd && isAsciiSpace(Text[Tail]))
+ ++Tail;
+ if (Tail < Lines[LastLine].ContentEnd && Text[Tail] != '#') {
+ diagnose(Tail, Lines[LastLine].ContentEnd, "unexpected characters after YAML value");
+ return {};
+ }
+ LineIndex = LastLine + 1;
+ return Result;
+ }
+
+ [[nodiscard]] NodeID parseInlineValue(std::size_t Begin, std::size_t End, std::size_t Depth) {
+ if (!checkDepth(Depth))
+ return {};
+ const std::string_view Text = Parsed.Source.text();
+ while (Begin < End && isAsciiSpace(Text[Begin]))
+ ++Begin;
+ if (Begin == End || Text[Begin] == '#')
+ return addNullNode(Begin, Begin);
+
+ std::size_t Cursor = Begin;
+ NodeID Result = parseFlowNode(Cursor, End, Depth, false);
+ if (Failed)
+ return {};
+
+ while (Cursor < End && isAsciiSpace(Text[Cursor]))
+ ++Cursor;
+ if (Cursor < End && Text[Cursor] != '#') {
+ diagnose(Cursor, End, "unexpected characters after YAML value");
+ return {};
+ }
+ return Result;
+ }
+
+ [[nodiscard]] NodeID parseFlowNode(std::size_t &Cursor, std::size_t End, std::size_t Depth,
+ bool IsFlowValue) {
+ if (!checkDepth(Depth))
+ return {};
+ const std::string_view Text = Parsed.Source.text();
+ while (Cursor < End && isAsciiSpace(Text[Cursor]))
+ ++Cursor;
+ if (Cursor >= End) {
+ diagnose(End, End, "expected YAML value");
+ return {};
+ }
+
+ if (Text[Cursor] == '[')
+ return parseFlowSequence(Cursor, End, Depth);
+ if (Text[Cursor] == '{')
+ return parseFlowMapping(Cursor, End, Depth);
+ if (Text[Cursor] == '\'' || Text[Cursor] == '"')
+ return parseQuotedScalar(Cursor, End);
+ if (Text[Cursor] == '&' || Text[Cursor] == '*' || Text[Cursor] == '!' || Text[Cursor] == '%') {
+ diagnose(Cursor, Cursor + 1, "YAML anchors, aliases, tags, and directives are not supported");
+ return {};
+ }
+
+ const std::size_t Begin = Cursor;
+ while (Cursor < End) {
+ const char Character = Text[Cursor];
+ if (IsFlowValue && (Character == ',' || Character == ']' || Character == '}'))
+ break;
+ if (Character == '#' && (Cursor == Begin || isAsciiSpace(Text[Cursor - 1])))
+ break;
+ ++Cursor;
+ }
+ std::size_t ScalarEnd = Cursor;
+ while (ScalarEnd > Begin && isAsciiSpace(Text[ScalarEnd - 1]))
+ --ScalarEnd;
+ if (ScalarEnd == Begin) {
+ diagnose(Begin, std::min(Begin + 1, End), "expected YAML scalar");
+ return {};
+ }
+ const std::string Value(Text.substr(Begin, ScalarEnd - Begin));
+ return isNullSpelling(Value) ? addNullNode(Begin, ScalarEnd)
+ : addScalarNode(Begin, ScalarEnd, ScalarStyle::Plain, Value);
+ }
+
+ [[nodiscard]] NodeID parseFlowSequence(std::size_t &Cursor, std::size_t End, std::size_t Depth) {
+ const std::size_t Begin = Cursor++;
+ std::vector<NodeID> Items;
+ const std::string_view Text = Parsed.Source.text();
+ skipFlowSpace(Cursor, End);
+ if (Cursor < End && Text[Cursor] == ']') {
+ ++Cursor;
+ return addSequenceNode(Begin, Cursor, CollectionStyle::Flow, std::move(Items));
+ }
+
+ while (Cursor < End) {
+ Items.push_back(parseFlowNode(Cursor, End, Depth + 1, true));
+ if (Failed)
+ return {};
+ skipFlowSpace(Cursor, End);
+ if (Cursor >= End) {
+ diagnose(Begin, End, "unterminated YAML flow sequence");
+ return {};
+ }
+ if (Text[Cursor] == ']') {
+ ++Cursor;
+ return addSequenceNode(Begin, Cursor, CollectionStyle::Flow, std::move(Items));
+ }
+ if (Text[Cursor] != ',') {
+ diagnose(Cursor, Cursor + 1, "expected ',' or ']' in YAML flow sequence");
+ return {};
+ }
+ ++Cursor;
+ skipFlowSpace(Cursor, End);
+ if (Cursor < End && Text[Cursor] == ']') {
+ ++Cursor;
+ return addSequenceNode(Begin, Cursor, CollectionStyle::Flow, std::move(Items));
+ }
+ }
+
+ diagnose(Begin, End, "unterminated YAML flow sequence");
+ return {};
+ }
+
+ [[nodiscard]] NodeID parseFlowMapping(std::size_t &Cursor, std::size_t End, std::size_t Depth) {
+ const std::size_t Begin = Cursor++;
+ std::vector<MappingEntry> Entries;
+ const std::string_view Text = Parsed.Source.text();
+ skipFlowSpace(Cursor, End);
+ if (Cursor < End && Text[Cursor] == '}') {
+ ++Cursor;
+ return addMappingNode(Begin, Cursor, CollectionStyle::Flow, std::move(Entries));
+ }
+
+ while (Cursor < End) {
+ std::string Key;
+ TextRange KeyRange;
+ if (!parseFlowKey(Cursor, End, Key, KeyRange))
+ return {};
+ if (std::any_of(Entries.begin(), Entries.end(),
+ [&Key](const MappingEntry &Entry) { return Entry.Key == Key; })) {
+ diagnose(KeyRange.Begin.Offset, KeyRange.End.Offset,
+ "duplicate YAML mapping key '" + Key + "'");
+ return {};
+ }
+ skipFlowSpace(Cursor, End);
+ if (Cursor >= End || Text[Cursor] != ':') {
+ diagnose(Cursor, std::min(Cursor + 1, End), "expected ':' after YAML flow mapping key");
+ return {};
+ }
+ ++Cursor;
+ skipFlowSpace(Cursor, End);
+ NodeID Value;
+ if (isFlowBlockSequenceStart(Cursor, End))
+ Value = parseFlowBlockSequence(Cursor, End, Depth + 1);
+ else if (Cursor < End && (Text[Cursor] == ',' || Text[Cursor] == '}'))
+ Value = addNullNode(Cursor, Cursor);
+ else
+ Value = parseFlowNode(Cursor, End, Depth + 1, true);
+ if (Failed)
+ return {};
+ Entries.push_back(MappingEntry{
+ .Key = std::move(Key),
+ .KeyRange = KeyRange,
+ .Value = Value,
+ });
+
+ skipFlowSpace(Cursor, End);
+ if (Cursor >= End) {
+ diagnose(Begin, End, "unterminated YAML flow mapping");
+ return {};
+ }
+ if (Text[Cursor] == '}') {
+ ++Cursor;
+ return addMappingNode(Begin, Cursor, CollectionStyle::Flow, std::move(Entries));
+ }
+ if (Text[Cursor] != ',') {
+ diagnose(Cursor, Cursor + 1, "expected ',' or '}' in YAML flow mapping");
+ return {};
+ }
+ ++Cursor;
+ skipFlowSpace(Cursor, End);
+ if (Cursor < End && Text[Cursor] == '}') {
+ ++Cursor;
+ return addMappingNode(Begin, Cursor, CollectionStyle::Flow, std::move(Entries));
+ }
+ }
+
+ diagnose(Begin, End, "unterminated YAML flow mapping");
+ return {};
+ }
+
+ [[nodiscard]] NodeID parseFlowBlockSequence(std::size_t &Cursor, std::size_t End,
+ std::size_t Depth) {
+ if (!checkDepth(Depth))
+ return {};
+ const std::size_t Begin = Cursor;
+ const std::size_t FirstLine = Parsed.Source.positionAt(Cursor).Line - 1;
+ const std::size_t Indent = Lines[FirstLine].Indent;
+ const std::string_view Text = Parsed.Source.text();
+ std::vector<NodeID> Items;
+ std::size_t LastItemEnd = Begin;
+
+ while (isFlowBlockSequenceStart(Cursor, End)) {
+ const std::size_t ItemLine = Parsed.Source.positionAt(Cursor).Line - 1;
+ if (Lines[ItemLine].Indent != Indent)
+ break;
+
+ const std::size_t Dash = Cursor++;
+ while (Cursor < Lines[ItemLine].ContentEnd && isAsciiSpace(Text[Cursor]))
+ ++Cursor;
+ if (Cursor == Lines[ItemLine].ContentEnd || Text[Cursor] == '#') {
+ diagnose(Dash, Lines[ItemLine].ContentEnd,
+ "empty block sequence entry inside a flow mapping is not supported");
+ return {};
+ }
+
+ const bool ItemCanSpan =
+ Text[Cursor] == '[' || Text[Cursor] == '{' || Text[Cursor] == '\'' || Text[Cursor] == '"';
+ const std::size_t ItemEnd = ItemCanSpan ? End : Lines[ItemLine].ContentEnd;
+ Items.push_back(parseFlowNode(Cursor, ItemEnd, Depth + 1, true));
+ if (Failed)
+ return {};
+ LastItemEnd = Cursor;
+ skipFlowSpace(Cursor, End);
+ }
+
+ return addSequenceNode(Begin, LastItemEnd, CollectionStyle::Block, std::move(Items));
+ }
+
+ [[nodiscard]] bool parseFlowKey(std::size_t &Cursor, std::size_t End, std::string &Key,
+ TextRange &Range) {
+ const std::string_view Text = Parsed.Source.text();
+ skipFlowSpace(Cursor, End);
+ if (Cursor >= End) {
+ diagnose(End, End, "expected YAML flow mapping key");
+ return false;
+ }
+
+ const std::size_t Begin = Cursor;
+ if (Text[Cursor] == '\'' || Text[Cursor] == '"') {
+ ScalarStyle Style = ScalarStyle::Plain;
+ return parseQuotedText(Cursor, End, Key, Style, Range);
+ }
+
+ while (Cursor < End && Text[Cursor] != ':' && Text[Cursor] != ',' && Text[Cursor] != '}')
+ ++Cursor;
+ std::size_t KeyEnd = Cursor;
+ while (KeyEnd > Begin && isAsciiSpace(Text[KeyEnd - 1]))
+ --KeyEnd;
+ if (KeyEnd == Begin) {
+ diagnose(Begin, std::min(Begin + 1, End), "empty YAML mapping keys are not supported");
+ return false;
+ }
+ Key = std::string(Text.substr(Begin, KeyEnd - Begin));
+ Range = Parsed.Source.range(Begin, KeyEnd);
+ return true;
+ }
+
+ [[nodiscard]] NodeID parseQuotedScalar(std::size_t &Cursor, std::size_t End) {
+ std::string Value;
+ ScalarStyle Style = ScalarStyle::Plain;
+ TextRange Range;
+ if (!parseQuotedText(Cursor, End, Value, Style, Range))
+ return {};
+ return addScalarNode(Range.Begin.Offset, Range.End.Offset, Style, std::move(Value));
+ }
+
+ [[nodiscard]] bool parseQuotedText(std::size_t &Cursor, std::size_t End, std::string &Value,
+ ScalarStyle &Style, TextRange &Range) {
+ const std::string_view Text = Parsed.Source.text();
+ const std::size_t Begin = Cursor;
+ const char Quote = Text[Cursor++];
+
+ while (Cursor < End) {
+ const char Character = Text[Cursor++];
+ if (Character == Quote) {
+ if (Quote == '\'' && Cursor < End && Text[Cursor] == '\'') {
+ Value.push_back('\'');
+ ++Cursor;
+ continue;
+ }
+ Style = Quote == '\'' ? ScalarStyle::SingleQuoted : ScalarStyle::DoubleQuoted;
+ Range = Parsed.Source.range(Begin, Cursor);
+ return true;
+ }
+
+ if (Quote == '\'' || Character != '\\') {
+ if (static_cast<unsigned char>(Character) < 0x20U && Character != '\t') {
+ diagnose(Cursor - 1, Cursor, "control character is not allowed in quoted YAML scalar");
+ return false;
+ }
+ Value.push_back(Character);
+ continue;
+ }
+
+ if (Cursor >= End) {
+ diagnose(Cursor - 1, Cursor, "unterminated escape in YAML scalar");
+ return false;
+ }
+ const char Escape = Text[Cursor++];
+ switch (Escape) {
+ case '0':
+ Value.push_back('\0');
+ break;
+ case 'a':
+ Value.push_back('\a');
+ break;
+ case 'b':
+ Value.push_back('\b');
+ break;
+ case 't':
+ case '\t':
+ Value.push_back('\t');
+ break;
+ case 'n':
+ Value.push_back('\n');
+ break;
+ case 'v':
+ Value.push_back('\v');
+ break;
+ case 'f':
+ Value.push_back('\f');
+ break;
+ case 'r':
+ Value.push_back('\r');
+ break;
+ case 'e':
+ Value.push_back(static_cast<char>(0x1B));
+ break;
+ case ' ':
+ Value.push_back(' ');
+ break;
+ case '"':
+ Value.push_back('"');
+ break;
+ case '/':
+ Value.push_back('/');
+ break;
+ case '\\':
+ Value.push_back('\\');
+ break;
+ case 'N':
+ appendUTF8(Value, 0x85U);
+ break;
+ case '_':
+ appendUTF8(Value, 0xA0U);
+ break;
+ case 'L':
+ appendUTF8(Value, 0x2028U);
+ break;
+ case 'P':
+ appendUTF8(Value, 0x2029U);
+ break;
+ case 'x':
+ if (!parseUnicodeEscape(Cursor, End, 2, Value))
+ return false;
+ break;
+ case 'u':
+ if (!parseUnicodeEscape(Cursor, End, 4, Value))
+ return false;
+ break;
+ case 'U':
+ if (!parseUnicodeEscape(Cursor, End, 8, Value))
+ return false;
+ break;
+ default:
+ diagnose(Cursor - 1, Cursor, "unknown escape in double-quoted YAML scalar");
+ return false;
+ }
+ }
+
+ diagnose(Begin, End, "unterminated quoted YAML scalar");
+ return false;
+ }
+
+ [[nodiscard]] bool parseUnicodeEscape(std::size_t &Cursor, std::size_t End,
+ std::size_t DigitCount, std::string &Output) {
+ if (End - Cursor < DigitCount) {
+ diagnose(Cursor, End, "incomplete Unicode escape in YAML scalar");
+ return false;
+ }
+ std::uint32_t CodePoint = 0;
+ for (std::size_t Index = 0; Index < DigitCount; ++Index) {
+ if (!isHexDigit(Parsed.Source.text()[Cursor + Index])) {
+ diagnose(Cursor + Index, Cursor + Index + 1,
+ "invalid hexadecimal digit in YAML Unicode escape");
+ return false;
+ }
+ CodePoint = (CodePoint << 4U) | hexValue(Parsed.Source.text()[Cursor + Index]);
+ }
+ Cursor += DigitCount;
+ if (CodePoint > 0x10FFFFU || (CodePoint >= 0xD800U && CodePoint <= 0xDFFFU)) {
+ diagnose(Cursor - DigitCount, Cursor, "invalid Unicode scalar value in YAML escape");
+ return false;
+ }
+ appendUTF8(Output, CodePoint);
+ return true;
+ }
+
+ [[nodiscard]] NodeID parseBlockScalar(std::size_t &LineIndex, std::size_t IndicatorStart,
+ std::size_t ParentIndent, std::size_t Depth,
+ std::size_t LimitLine) {
+ if (!checkDepth(Depth))
+ return {};
+ const std::string_view Text = Parsed.Source.text();
+ const char Indicator = Text[IndicatorStart];
+ char Chomping = '\0';
+ std::optional<unsigned> ExplicitIndent;
+ std::size_t Cursor = IndicatorStart + 1;
+ while (Cursor < Lines[LineIndex].ContentEnd && !isAsciiSpace(Text[Cursor]) &&
+ Text[Cursor] != '#') {
+ const char Header = Text[Cursor++];
+ if ((Header == '+' || Header == '-') && Chomping == '\0') {
+ Chomping = Header;
+ continue;
+ }
+ if (Header >= '1' && Header <= '9' && !ExplicitIndent) {
+ ExplicitIndent = static_cast<unsigned>(Header - '0');
+ continue;
+ }
+ diagnose(Cursor - 1, Cursor, "invalid YAML block scalar header");
+ return {};
+ }
+ while (Cursor < Lines[LineIndex].ContentEnd && isAsciiSpace(Text[Cursor]))
+ ++Cursor;
+ if (Cursor < Lines[LineIndex].ContentEnd && Text[Cursor] != '#') {
+ diagnose(Cursor, Lines[LineIndex].ContentEnd,
+ "unexpected characters after YAML block scalar header");
+ return {};
+ }
+
+ const std::size_t HeaderLine = LineIndex;
+ std::size_t FirstContentLine = LineIndex + 1;
+ std::size_t FirstNonBlank = FirstContentLine;
+ while (FirstNonBlank < LimitLine && isWhitespaceOnlyLine(FirstNonBlank))
+ ++FirstNonBlank;
+
+ std::size_t ContentIndent = ParentIndent + 1;
+ if (ExplicitIndent)
+ ContentIndent = ParentIndent + *ExplicitIndent;
+ else if (FirstNonBlank < LimitLine)
+ ContentIndent = Lines[FirstNonBlank].Indent;
+
+ if (FirstNonBlank < LimitLine && Lines[FirstNonBlank].Indent <= ParentIndent) {
+ FirstContentLine = FirstNonBlank;
+ ContentIndent = ParentIndent + 1;
+ } else if (FirstNonBlank < LimitLine && Lines[FirstNonBlank].Indent < ContentIndent) {
+ diagnose(Lines[FirstNonBlank].Begin, Lines[FirstNonBlank].ContentEnd,
+ "block scalar content is less indented than its header");
+ return {};
+ }
+
+ struct ScalarLine final {
+ std::string Text;
+ bool HasLineBreak = false;
+ bool IsBlank = false;
+ bool IsMoreIndented = false;
+ };
+ std::vector<ScalarLine> ScalarLines;
+ std::size_t CurrentLine = FirstContentLine;
+ while (CurrentLine < LimitLine) {
+ const PhysicalLine &Line = Lines[CurrentLine];
+ const bool IsWhitespaceOnly = isWhitespaceOnlyLine(CurrentLine);
+ if (!IsWhitespaceOnly && Line.Indent < ContentIndent)
+ break;
+
+ ScalarLine ScalarPart;
+ ScalarPart.HasLineBreak = Line.End > Line.ContentEnd;
+ ScalarPart.IsBlank = IsWhitespaceOnly;
+ ScalarPart.IsMoreIndented = !IsWhitespaceOnly && Line.Indent > ContentIndent;
+ if (!IsWhitespaceOnly) {
+ const std::size_t TextBegin = Line.Begin + ContentIndent;
+ ScalarPart.Text = std::string(Text.substr(TextBegin, Line.ContentEnd - TextBegin));
+ }
+ ScalarLines.push_back(std::move(ScalarPart));
+ ++CurrentLine;
+ }
+
+ std::string Value;
+ for (std::size_t Index = 0; Index < ScalarLines.size(); ++Index) {
+ const ScalarLine &Line = ScalarLines[Index];
+ Value += Line.Text;
+ if (!Line.HasLineBreak)
+ continue;
+
+ bool FoldBreak = false;
+ if (Indicator == '>' && Index + 1 < ScalarLines.size()) {
+ const ScalarLine &Next = ScalarLines[Index + 1];
+ FoldBreak = !Line.IsBlank && !Next.IsBlank && !Line.IsMoreIndented && !Next.IsMoreIndented;
+ }
+ Value.push_back(FoldBreak ? ' ' : '\n');
+ }
+
+ if (Chomping != '+') {
+ while (!Value.empty() && Value.back() == '\n')
+ Value.pop_back();
+ if (Chomping != '-' && !ScalarLines.empty() && ScalarLines.back().HasLineBreak)
+ Value.push_back('\n');
+ }
+
+ LineIndex = CurrentLine;
+ const std::size_t End = CurrentLine > FirstContentLine ? Lines[CurrentLine - 1].ContentEnd
+ : Lines[HeaderLine].ContentEnd;
+ return addScalarNode(IndicatorStart, End,
+ Indicator == '|' ? ScalarStyle::Literal : ScalarStyle::Folded,
+ std::move(Value));
+ }
+
+ [[nodiscard]] bool parseMappingKey(std::size_t Begin, std::size_t End, std::string &Key,
+ TextRange &Range) {
+ const std::string_view Text = Parsed.Source.text();
+ while (Begin < End && isAsciiSpace(Text[Begin]))
+ ++Begin;
+ while (End > Begin && isAsciiSpace(Text[End - 1]))
+ --End;
+ if (Begin == End) {
+ diagnose(Begin, End, "empty YAML mapping keys are not supported");
+ return false;
+ }
+
+ if (Text[Begin] == '\'' || Text[Begin] == '"') {
+ std::size_t Cursor = Begin;
+ ScalarStyle Style = ScalarStyle::Plain;
+ if (!parseQuotedText(Cursor, End, Key, Style, Range))
+ return false;
+ while (Cursor < End && isAsciiSpace(Text[Cursor]))
+ ++Cursor;
+ if (Cursor != End) {
+ diagnose(Cursor, End, "unexpected characters after quoted YAML mapping key");
+ return false;
+ }
+ return true;
+ }
+ if (Text[Begin] == '?' || Text[Begin] == '[' || Text[Begin] == '{') {
+ diagnose(Begin, End, "complex YAML mapping keys are not supported");
+ return false;
+ }
+ Key = std::string(Text.substr(Begin, End - Begin));
+ Range = Parsed.Source.range(Begin, End);
+ return true;
+ }
+
+ [[nodiscard]] std::optional<std::size_t> findBlockMappingColon(std::size_t Begin,
+ std::size_t End) const noexcept {
+ const std::string_view Text = Parsed.Source.text();
+ char Quote = '\0';
+ unsigned FlowDepth = 0;
+ for (std::size_t Cursor = Begin; Cursor < End; ++Cursor) {
+ const char Character = Text[Cursor];
+ if (Quote == '\'') {
+ if (Character == '\'' && Cursor + 1 < End && Text[Cursor + 1] == '\'') {
+ ++Cursor;
+ } else if (Character == '\'') {
+ Quote = '\0';
+ }
+ continue;
+ }
+ if (Quote == '"') {
+ if (Character == '\\' && Cursor + 1 < End) {
+ ++Cursor;
+ } else if (Character == '"') {
+ Quote = '\0';
+ }
+ continue;
+ }
+ if (Character == '\'' || Character == '"') {
+ Quote = Character;
+ continue;
+ }
+ if (Character == '[' || Character == '{') {
+ ++FlowDepth;
+ continue;
+ }
+ if ((Character == ']' || Character == '}') && FlowDepth > 0) {
+ --FlowDepth;
+ continue;
+ }
+ if (Character == '#' && (Cursor == Begin || isAsciiSpace(Text[Cursor - 1])))
+ return std::nullopt;
+ if (Character != ':' || FlowDepth != 0)
+ continue;
+ if (Cursor + 1 == End || isAsciiSpace(Text[Cursor + 1]) || Text[Cursor + 1] == '#')
+ return Cursor;
+ }
+ return std::nullopt;
+ }
+
+ [[nodiscard]] bool isSequenceIndicator(std::size_t Offset, std::size_t End) const noexcept {
+ if (Offset >= End || Parsed.Source.text()[Offset] != '-')
+ return false;
+ return Offset + 1 == End || isAsciiSpace(Parsed.Source.text()[Offset + 1]) ||
+ Parsed.Source.text()[Offset + 1] == '#';
+ }
+
+ void skipFlowSpace(std::size_t &Cursor, std::size_t End) const noexcept {
+ const std::string_view Text = Parsed.Source.text();
+ while (Cursor < End) {
+ if (isAsciiSpace(Text[Cursor]) || Text[Cursor] == '\r' || Text[Cursor] == '\n') {
+ ++Cursor;
+ continue;
+ }
+ if (Text[Cursor] != '#')
+ return;
+ while (Cursor < End && Text[Cursor] != '\n')
+ ++Cursor;
+ }
+ }
+
+ [[nodiscard]] bool isFlowBlockSequenceStart(std::size_t Cursor, std::size_t End) const noexcept {
+ if (Cursor >= End)
+ return false;
+ const std::size_t LineIndex = Parsed.Source.positionAt(Cursor).Line - 1;
+ if (LineIndex >= Lines.size())
+ return false;
+ const PhysicalLine &Line = Lines[LineIndex];
+ return Cursor == Line.Begin + Line.Indent && isSequenceIndicator(Cursor, Line.ContentEnd);
+ }
+
+ [[nodiscard]] bool isWhitespaceOnlyLine(std::size_t LineIndex) const noexcept {
+ const PhysicalLine &Line = Lines[LineIndex];
+ const std::string_view Text = Parsed.Source.text();
+ for (std::size_t Cursor = Line.Begin; Cursor < Line.ContentEnd; ++Cursor) {
+ if (!isAsciiSpace(Text[Cursor]))
+ return false;
+ }
+ return true;
+ }
+
+ [[nodiscard]] bool checkDepth(std::size_t Depth) {
+ if (Depth <= Options.MaximumDepth)
+ return true;
+ diagnose(0, 0, "maximum YAML nesting depth exceeded");
+ return false;
+ }
+
+ [[nodiscard]] NodeID addNullNode(std::size_t Begin, std::size_t End) {
+ return addNode(Node(NodeKind::Null, Parsed.Source.range(Begin, End)));
+ }
+
+ [[nodiscard]] NodeID addScalarNode(std::size_t Begin, std::size_t End, ScalarStyle Style,
+ std::string Value) {
+ Node NewNode(NodeKind::Scalar, Parsed.Source.range(Begin, End));
+ NewNode.ScalarStyleValue = Style;
+ NewNode.Scalar = std::move(Value);
+ return addNode(std::move(NewNode));
+ }
+
+ [[nodiscard]] NodeID addSequenceNode(std::size_t Begin, std::size_t End, CollectionStyle Style,
+ std::vector<NodeID> Items) {
+ Node NewNode(NodeKind::Sequence, Parsed.Source.range(Begin, End));
+ NewNode.CollectionStyleValue = Style;
+ NewNode.Sequence = std::move(Items);
+ return addNode(std::move(NewNode));
+ }
+
+ [[nodiscard]] NodeID addMappingNode(std::size_t Begin, std::size_t End, CollectionStyle Style,
+ std::vector<MappingEntry> Entries) {
+ Node NewNode(NodeKind::Mapping, Parsed.Source.range(Begin, End));
+ NewNode.CollectionStyleValue = Style;
+ NewNode.Mapping = std::move(Entries);
+ return addNode(std::move(NewNode));
+ }
+
+ [[nodiscard]] NodeID addNode(Node NewNode) {
+ if (Parsed.Nodes.size() >= Options.MaximumNodes) {
+ diagnose(NewNode.range().Begin.Offset, NewNode.range().End.Offset,
+ "maximum YAML node count exceeded");
+ return {};
+ }
+ const auto RawID = static_cast<std::uint32_t>(Parsed.Nodes.size());
+ Parsed.Nodes.push_back(std::move(NewNode));
+ return NodeID(RawID);
+ }
+
+ Stream Parsed;
+ ParseOptions Options;
+ std::vector<PhysicalLine> Lines;
+ std::vector<TextDiagnostic> Diagnostics;
+ bool Failed = false;
+};
+
+ParseResult parse(std::string SourceName, std::string SourceText, ParseOptions Options) {
+ return Parser(std::move(SourceName), std::move(SourceText), Options).run();
+}
+
+} // namespace rcc::yaml
diff --git a/backend/unittests/Support/CMakeLists.txt b/backend/unittests/Support/CMakeLists.txt
index 0c0aabc..50a1e7c 100644
--- a/backend/unittests/Support/CMakeLists.txt
+++ b/backend/unittests/Support/CMakeLists.txt
@@ -1,16 +1,30 @@
# SPDX-License-Identifier: GPL-3.0-or-later
-set(RCCBackendSupportTestSources ErrorTest.cpp)
-
-add_executable(RCCBackendSupportTests ${RCCBackendSupportTestSources})
+add_executable(RCCBackendSupportTests ErrorTest.cpp)
rcc_backend_configure_target(RCCBackendSupportTests)
target_link_libraries(RCCBackendSupportTests PRIVATE RCCBackend::Support)
add_test(NAME RCCBackend.Support.Error COMMAND RCCBackendSupportTests)
+add_executable(RCCBackendTextDiagnosticTests TextDiagnosticTest.cpp)
+rcc_backend_configure_target(RCCBackendTextDiagnosticTests)
+target_link_libraries(RCCBackendTextDiagnosticTests PRIVATE RCCBackend::Support)
+
+add_test(NAME RCCBackend.Support.TextDiagnostic COMMAND RCCBackendTextDiagnosticTests)
+
+add_executable(RCCBackendYAMLTests YAMLTest.cpp)
+rcc_backend_configure_target(RCCBackendYAMLTests)
+target_link_libraries(RCCBackendYAMLTests PRIVATE RCCBackend::Support)
+
+add_test(NAME RCCBackend.Support.YAML COMMAND RCCBackendYAMLTests)
+
rcc_backend_register_format_files(
"${CMAKE_CURRENT_SOURCE_DIR}/ErrorTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/TextDiagnosticTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/YAMLTest.cpp"
)
rcc_backend_register_tidy_files(
"${CMAKE_CURRENT_SOURCE_DIR}/ErrorTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/TextDiagnosticTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/YAMLTest.cpp"
)
diff --git a/backend/unittests/Support/TextDiagnosticTest.cpp b/backend/unittests/Support/TextDiagnosticTest.cpp
new file mode 100644
index 0000000..cd28027
--- /dev/null
+++ b/backend/unittests/Support/TextDiagnosticTest.cpp
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/Support/TextDiagnostic.h"
+
+#include <iostream>
+#include <string>
+
+namespace {
+
+int fail(const char *Message) {
+ std::cerr << Message << '\n';
+ return 1;
+}
+
+} // namespace
+
+int main() {
+ rcc::TextSource Source("input.mir", "first\r\nsecond line\n");
+ if (Source.lineCount() != 3)
+ return fail("TextSource must index every physical line");
+ if (Source.line(1) != "first" || Source.line(2) != "second line" || !Source.line(3).empty())
+ return fail("TextSource must expose lines without line endings");
+
+ const rcc::TextRange Range = Source.range(9, 15);
+ if (Range.Begin.Line != 2 || Range.Begin.Column != 3 || Range.End.Line != 2 ||
+ Range.End.Column != 9)
+ return fail("TextSource must map byte offsets to one-based line and column values");
+
+ const rcc::TextDiagnostic Diagnostic{
+ .Severity = rcc::DiagnosticSeverity::Error,
+ .Category = "yaml",
+ .Message = "invalid scalar",
+ .SourceName = "input.mir",
+ .Range = Range,
+ };
+ const std::string Formatted = rcc::formatTextDiagnostic(Diagnostic, Source);
+ if (Formatted != "input.mir:2:3: error[yaml]: invalid scalar\nsecond line\n ^~~~~~")
+ return fail("Text diagnostics must have deterministic source rendering");
+
+ if (rcc::getDiagnosticSeverityName(rcc::DiagnosticSeverity::Warning) != "warning")
+ return fail("Diagnostic severities must have stable names");
+ return 0;
+}
diff --git a/backend/unittests/Support/YAMLTest.cpp b/backend/unittests/Support/YAMLTest.cpp
new file mode 100644
index 0000000..445159d
--- /dev/null
+++ b/backend/unittests/Support/YAMLTest.cpp
@@ -0,0 +1,232 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/Support/YAML.h"
+
+#include <iostream>
+#include <string>
+#include <string_view>
+
+namespace {
+
+int fail(const char *Message) {
+ std::cerr << Message << '\n';
+ return 1;
+}
+
+const rcc::yaml::Node *mappingValue(const rcc::yaml::Stream &Stream, const rcc::yaml::Node &Mapping,
+ std::string_view Key) {
+ const std::optional<rcc::yaml::NodeID> Value = Mapping.mappingValue(Key);
+ return Value ? Stream.node(*Value) : nullptr;
+}
+
+int testMIRShape() {
+ const std::string Input =
+ "--- |\n"
+ " define void @f() {\n"
+ " entry:\n"
+ " ret void\n"
+ " }\n"
+ "---\n"
+ "name: f\n"
+ "alignment: 16\n"
+ "registers:\n"
+ " - { id: 0, class: gpr, flags: [foo, 'bar',], preferred-register:, }\n"
+ "frameInfo:\n"
+ " hasCalls: true\n"
+ "jumpTable:\n"
+ " blocks: [ '%bb.0',\n"
+ " '%bb.1', ]\n"
+ "callSites:\n"
+ " - { bb: 0, offset: 4, fwdArgRegs:\n"
+ " - { arg: 0, reg: '$x1' }\n"
+ " - { arg: 1, reg: '$x2' } }\n"
+ "body: |-\n"
+ " bb.0:\n"
+ " RET\n"
+ "...\n";
+
+ rcc::yaml::ParseResult Result = rcc::yaml::parse("input.mir", Input);
+ const rcc::yaml::Stream *ParsedStream = Result.stream();
+ if (!Result.succeeded() || ParsedStream == nullptr)
+ return fail("YAML parser must accept the LLVM MIR document shape");
+
+ const rcc::yaml::Stream &Stream = *ParsedStream;
+ if (Stream.documents().size() != 2)
+ return fail("YAML parser must retain every document in input order");
+
+ const rcc::yaml::Node *IR = Stream.node(Stream.documents()[0].Root);
+ if (IR == nullptr || IR->kind() != rcc::yaml::NodeKind::Scalar ||
+ IR->scalarStyle() != rcc::yaml::ScalarStyle::Literal ||
+ IR->scalar() != "define void @f() {\nentry:\n ret void\n}\n")
+ return fail("YAML parser must decode the embedded LLVM IR block scalar");
+
+ const rcc::yaml::Node *Function = Stream.node(Stream.documents()[1].Root);
+ if (Function == nullptr || Function->kind() != rcc::yaml::NodeKind::Mapping)
+ return fail("YAML parser must represent a MachineFunction document as a mapping");
+
+ const rcc::yaml::Node *Name = mappingValue(Stream, *Function, "name");
+ const rcc::yaml::Node *Registers = mappingValue(Stream, *Function, "registers");
+ const rcc::yaml::Node *FrameInfo = mappingValue(Stream, *Function, "frameInfo");
+ const rcc::yaml::Node *JumpTable = mappingValue(Stream, *Function, "jumpTable");
+ const rcc::yaml::Node *CallSites = mappingValue(Stream, *Function, "callSites");
+ const rcc::yaml::Node *Body = mappingValue(Stream, *Function, "body");
+ if (Name == nullptr || Name->scalar() != "f")
+ return fail("YAML mapping lookup must preserve scalar values");
+ if (Registers == nullptr || Registers->sequence().size() != 1)
+ return fail("YAML parser must decode block and flow collections together");
+ if (FrameInfo == nullptr || FrameInfo->kind() != rcc::yaml::NodeKind::Mapping)
+ return fail("YAML parser must decode nested block mappings");
+ const rcc::yaml::Node *Blocks =
+ JumpTable == nullptr ? nullptr : mappingValue(Stream, *JumpTable, "blocks");
+ if (Blocks == nullptr || Blocks->sequence().size() != 2)
+ return fail("YAML parser must decode line-wrapped flow sequences");
+ const rcc::yaml::Node *CallSite =
+ CallSites == nullptr ? nullptr : Stream.node(CallSites->sequence()[0]);
+ const rcc::yaml::Node *Forwarded =
+ CallSite == nullptr ? nullptr : mappingValue(Stream, *CallSite, "fwdArgRegs");
+ if (Forwarded == nullptr || Forwarded->sequence().size() != 2)
+ return fail("YAML parser must decode block sequences nested in line-wrapped flow mappings");
+ if (Body == nullptr || Body->scalar() != "bb.0:\n RET")
+ return fail("YAML block scalar strip chomping must remove trailing line breaks");
+
+ const rcc::yaml::Node *Register = Stream.node(Registers->sequence()[0]);
+ const rcc::yaml::Node *Flags =
+ Register == nullptr ? nullptr : mappingValue(Stream, *Register, "flags");
+ if (Register == nullptr || Register->collectionStyle() != rcc::yaml::CollectionStyle::Flow ||
+ Flags == nullptr || Flags->sequence().size() != 2)
+ return fail("YAML flow mapping and sequence structure must be retained");
+ return 0;
+}
+
+int testScalarRules() {
+ const std::string Input = "plain: value # comment\n"
+ "single: 'it''s YAML'\n"
+ "double: \"line\\n\\u03bb\"\n"
+ "nothing: null\n"
+ "literal: |-\n"
+ " \tcontent\n"
+ "hash: |-\n"
+ " # scalar content\n"
+ "folded: >-\n"
+ " one\n"
+ " two\n";
+ rcc::yaml::ParseResult Result = rcc::yaml::parse("scalars.yaml", Input);
+ const rcc::yaml::Stream *ParsedStream = Result.stream();
+ if (!Result.succeeded() || ParsedStream == nullptr)
+ return fail("YAML parser must accept supported scalar forms");
+
+ const rcc::yaml::Stream &Stream = *ParsedStream;
+ const rcc::yaml::Node *Root = Stream.node(Stream.documents()[0].Root);
+ if (Root == nullptr)
+ return fail("YAML scalar test must have a root");
+ const rcc::yaml::Node *Single = mappingValue(Stream, *Root, "single");
+ const rcc::yaml::Node *Double = mappingValue(Stream, *Root, "double");
+ const rcc::yaml::Node *Nothing = mappingValue(Stream, *Root, "nothing");
+ const rcc::yaml::Node *Literal = mappingValue(Stream, *Root, "literal");
+ const rcc::yaml::Node *Hash = mappingValue(Stream, *Root, "hash");
+ const rcc::yaml::Node *Folded = mappingValue(Stream, *Root, "folded");
+ if (Single == nullptr || Single->scalar() != "it's YAML")
+ return fail("YAML single quotes must decode doubled quotes");
+ if (Double == nullptr || Double->scalar() != "line\n\xCE\xBB")
+ return fail("YAML double quotes must decode escapes and Unicode");
+ if (Nothing == nullptr || Nothing->kind() != rcc::yaml::NodeKind::Null)
+ return fail("YAML null spellings must produce null nodes");
+ if (Literal == nullptr || Literal->scalar() != "\tcontent")
+ return fail("YAML block scalars must preserve tab characters in scalar content");
+ if (Hash == nullptr || Hash->scalar() != "# scalar content")
+ return fail("YAML block scalar content must not interpret '#' as a comment");
+ if (Folded == nullptr || Folded->scalar() != "one two")
+ return fail("YAML folded scalars must fold ordinary line breaks");
+ return 0;
+}
+
+int testNestedSequenceMapping() {
+ const std::string Input = "savePoint:\n"
+ " - point: '%bb.1'\n"
+ " registers:\n"
+ " - '$x1'\n"
+ " - '$x2'\n";
+ rcc::yaml::ParseResult Result = rcc::yaml::parse("nested.yaml", Input);
+ const rcc::yaml::Stream *ParsedStream = Result.stream();
+ if (!Result.succeeded() || ParsedStream == nullptr)
+ return fail("YAML parser must accept sequence entries that start block mappings");
+
+ const rcc::yaml::Stream &Stream = *ParsedStream;
+ const rcc::yaml::Node *Root = Stream.node(Stream.documents()[0].Root);
+ const rcc::yaml::Node *SavePoints =
+ Root == nullptr ? nullptr : mappingValue(Stream, *Root, "savePoint");
+ const rcc::yaml::Node *SavePoint =
+ SavePoints == nullptr ? nullptr : Stream.node(SavePoints->sequence()[0]);
+ const rcc::yaml::Node *Registers =
+ SavePoint == nullptr ? nullptr : mappingValue(Stream, *SavePoint, "registers");
+ if (Registers == nullptr || Registers->sequence().size() != 2)
+ return fail("YAML nested sequence mappings must retain continuation keys");
+ return 0;
+}
+
+int testDiagnostics() {
+ rcc::yaml::ParseResult Duplicate = rcc::yaml::parse("duplicate.yaml", "name: a\nname: b\n");
+ if (Duplicate.succeeded() || Duplicate.diagnostics().size() != 1 ||
+ Duplicate.diagnostics()[0].Message != "duplicate YAML mapping key 'name'" ||
+ Duplicate.diagnostics()[0].Range.Begin.Line != 2)
+ return fail("YAML parser must reject duplicate keys at the second definition");
+
+ rcc::yaml::ParseResult BadIndent = rcc::yaml::parse("tab.yaml", "root:\n\tchild: value\n");
+ if (BadIndent.succeeded() || BadIndent.diagnostics().empty() ||
+ BadIndent.diagnostics()[0].Message != "tab characters are not allowed in YAML indentation")
+ return fail("YAML parser must reject tab indentation");
+
+ rcc::yaml::ParseResult BadFlow = rcc::yaml::parse("flow.yaml", "values: [one two\n");
+ if (BadFlow.succeeded() || BadFlow.diagnostics().empty() ||
+ BadFlow.diagnostics()[0].Message != "unterminated YAML flow sequence")
+ return fail("YAML parser must diagnose unterminated flow collections");
+
+ rcc::yaml::ParseResult BadEnd = rcc::yaml::parse("end.yaml", "---\nname: f\n... garbage\n");
+ if (BadEnd.succeeded() || BadEnd.diagnostics().empty() ||
+ BadEnd.diagnostics()[0].Message != "document end marker may only be followed by a comment")
+ return fail("YAML parser must reject content after a document end marker");
+
+ rcc::yaml::ParseResult Directive = rcc::yaml::parse("directive.yaml", "%YAML 1.2\n---\na\n");
+ if (Directive.succeeded() || Directive.diagnostics().empty() ||
+ Directive.diagnostics()[0].Message != "YAML directives are not supported")
+ return fail("YAML parser must explicitly reject unsupported directives");
+
+ rcc::yaml::ParseResult Alias = rcc::yaml::parse("alias.yaml", "value: *shared\n");
+ if (Alias.succeeded() || Alias.diagnostics().empty() ||
+ Alias.diagnostics()[0].Message !=
+ "YAML anchors, aliases, tags, and directives are not supported")
+ return fail("YAML parser must explicitly reject unsupported aliases");
+
+ const std::string NulInput("name:\0f", 7);
+ rcc::yaml::ParseResult Nul = rcc::yaml::parse("nul.yaml", NulInput);
+ if (Nul.succeeded() || Nul.diagnostics().empty() ||
+ Nul.diagnostics()[0].Message != "NUL bytes are not allowed in YAML input")
+ return fail("YAML parser must reject embedded NUL bytes");
+
+ rcc::yaml::ParseOptions Limits;
+ Limits.MaximumDepth = 2;
+ rcc::yaml::ParseResult Deep = rcc::yaml::parse("deep.yaml", "a:\n b:\n c: value\n", Limits);
+ if (Deep.succeeded() || Deep.diagnostics().empty() ||
+ Deep.diagnostics()[0].Message != "maximum YAML nesting depth exceeded")
+ return fail("YAML parser must enforce its nesting limit");
+
+ Limits.MaximumDepth = 128;
+ Limits.MaximumNodes = 1;
+ rcc::yaml::ParseResult ManyNodes = rcc::yaml::parse("nodes.yaml", "name: f\n", Limits);
+ if (ManyNodes.succeeded() || ManyNodes.diagnostics().empty() ||
+ ManyNodes.diagnostics()[0].Message != "maximum YAML node count exceeded")
+ return fail("YAML parser must enforce its node limit");
+ return 0;
+}
+
+} // namespace
+
+int main() {
+ if (const int Result = testMIRShape(); Result != 0)
+ return Result;
+ if (const int Result = testScalarRules(); Result != 0)
+ return Result;
+ if (const int Result = testNestedSequenceMapping(); Result != 0)
+ return Result;
+ return testDiagnostics();
+}