diff options
| author | Richard-Qin-X <richard.qin.001@gmail.com> | 2026-08-01 16:47:16 +0800 |
|---|---|---|
| committer | Richard-Qin-X <richard.qin.001@gmail.com> | 2026-08-01 16:47:16 +0800 |
| commit | 85eed8fb2d6563fea750f7b55730c9feaecc5898 (patch) | |
| tree | 81663a7028c73e245ce573cd6b52e2a751146cd6 | |
| parent | 924dcd36ebed84f04729de05ec08b311a36a04c1 (diff) | |
| download | rcc-project-85eed8fb2d6563fea750f7b55730c9feaecc5898.tar.gz rcc-project-85eed8fb2d6563fea750f7b55730c9feaecc5898.tar.xz | |
[Support] Add MIR YAML parsing infrastructure
Introduce source ranges, deterministic text diagnostics, and an independently authored YAML stream parser for the fixed LLVM MIR container shapes. Record the compatibility matrix and Stage 6 validation state.
| -rw-r--r-- | backend/include/rcc/Support/TextDiagnostic.h | 83 | ||||
| -rw-r--r-- | backend/include/rcc/Support/YAML.h | 197 | ||||
| -rw-r--r-- | backend/lib/Support/CMakeLists.txt | 12 | ||||
| -rw-r--r-- | backend/lib/Support/TextDiagnostic.cpp | 91 | ||||
| -rw-r--r-- | backend/lib/Support/YAML.cpp | 1217 | ||||
| -rw-r--r-- | backend/unittests/Support/CMakeLists.txt | 20 | ||||
| -rw-r--r-- | backend/unittests/Support/TextDiagnosticTest.cpp | 43 | ||||
| -rw-r--r-- | backend/unittests/Support/YAMLTest.cpp | 232 |
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(); +} |
