diff options
Diffstat (limited to 'backend')
| -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(); +} |
