Skip to content

Commit 9835c31

Browse files
committed
In several PRs, such as microsoft#908 and microsoft#1210 , it has become clear that we need a thread safe channel through which errors and warnings can both be reasonably reported. Now that microsoft#1279 is landed and functionally everything in the codebase uses ExpectedL, we can look at what the new thing that fixes issues is.
Consider the following: ```c++ ExpectedL<T> example_api(int a); ExpectedL<std::unique_ptr<SourceControlFile>> try_load_port_manifest_text(StringView text, StringView control_path, MessageSink& warning_sink); ``` The reason this can't return the warnings through the ExpectedL channel is that we don't want the 'error' state to be engaged when there are merely warnings. Moreover, that these channels are different channels means that situations that might want to return errors and warnings together, as happens when parsing files, means that order relationships between errors and warnings is lost. It is probably a good idea in general to put warnings and errors about the same location next to each other in the output, but that's hard to do with this interface. Rather than multiplexing everything through the return value, this proposal is to multiplex only the success or failure through the return value, and report any specific error information through an out parameter. 1. Distinguish whether an overall operation succeeded or failed in the return value, but 2. record any errors or warnings via an out parameter. Applying this to the above gives: ```c++ Optional<T> example_api(MessageContext& context, int a); // unique_ptr is already 'optional' std::unique_ptr<SourceControlFile> try_load_port_manifest_text(MessageContext& context, StringView text, StringView control_path); ``` Issues this new mechanism fixes: * Errors and warnings can share the same channel and thus be printed together * The interface between code wanting to report events and the code wanting to consume them is a natural thread synchronization boundary. Other attempts to fix this have been incorrect by synchronizing individual print calls ( microsoft#1290 ) or complex enough that we are not sure they are correct by trying to recover boundaries by reparsing our own error output ( microsoft#908 ) * This shuts down the "error: error:" and similar bugs where it isn't clear who is formatting the overall error message vs. talking about individual components Known issues that are not fixed by this change: * This still doesn't make it easy for callers to programmatically handle specific types of errors. Currently, we have some APIs that still use explicit `std::error_code` because they want to do different things for 'file does not exist' vs. 'there was an I/O error'. Given that this condition isn't well served by the ExpectedL mechanism I don't want to wait until we have a better solution to it to proceed. * Because we aren't making the context parameter the 'success carrier' it's more complex to implement 'warnings as errors' or similar functionality where the caller decides how 'important' something is. I would be in favor of moving all success tests to the context parameter but I'm not proposing that because the other vcpkg maintainers do not like it. * Contextual information / stack problems aren't solved. However, the context parameter might be extended in the future to help with this.
1 parent bfda089 commit 9835c31

13 files changed

+394
-52
lines changed

include/vcpkg/base/diagnostics.h

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#pragma once
2+
3+
#include <vcpkg/base/expected.h>
4+
#include <vcpkg/base/messages.h>
5+
#include <vcpkg/base/optional.h>
6+
7+
#include <mutex>
8+
#include <string>
9+
#include <type_traits>
10+
#include <vector>
11+
12+
namespace vcpkg
13+
{
14+
enum class DiagKind
15+
{
16+
None, // foo.h: localized
17+
Message, // foo.h: message: localized
18+
Error, // foo.h: error: localized
19+
Warning, // foo.h: warning: localized
20+
Note, // foo.h: note: localized
21+
COUNT
22+
};
23+
24+
struct TextPosition
25+
{
26+
// '0' indicates uninitialized; '1' is the first row/column
27+
int row = 0;
28+
int column = 0;
29+
};
30+
31+
struct DiagnosticLine
32+
{
33+
DiagnosticLine(DiagKind kind, LocalizedString&& message)
34+
: m_kind(kind), m_origin(), m_position(), m_message(std::move(message))
35+
{
36+
}
37+
38+
DiagnosticLine(DiagKind kind, StringView origin, LocalizedString&& message)
39+
: m_kind(kind), m_origin(origin.to_string()), m_position(), m_message(std::move(message))
40+
{
41+
}
42+
43+
DiagnosticLine(DiagKind kind, StringView origin, TextPosition position, LocalizedString&& message)
44+
: m_kind(kind), m_origin(origin.to_string()), m_position(position), m_message(std::move(message))
45+
{
46+
}
47+
48+
// Prints this diagnostic to the terminal.
49+
// Not thread safe: The console DiagnosticContext must apply its own synchronization.
50+
void print(MessageSink& sink) const;
51+
// Converts this message into a string
52+
// Prefer print() if possible because it applies color
53+
std::string to_string() const;
54+
void to_string(std::string& target) const;
55+
56+
private:
57+
DiagKind m_kind;
58+
Optional<std::string> m_origin;
59+
TextPosition m_position;
60+
LocalizedString m_message;
61+
};
62+
63+
struct DiagnosticContext
64+
{
65+
// Records a diagnostic. Implementations must make simultaneous calls of report() safe from multiple threads
66+
// and print entire DiagnosticLines as atomic units. Implementations are not required to synchronize with
67+
// other machinery like msg::print and friends.
68+
//
69+
// This serves to make multithreaded code that reports only via this mechanism safe.
70+
virtual void report(const DiagnosticLine& line) = 0;
71+
virtual void report(DiagnosticLine&& line) { report(line); }
72+
73+
protected:
74+
~DiagnosticContext() = default;
75+
};
76+
77+
struct BufferedDiagnosticContext final : DiagnosticContext
78+
{
79+
virtual void report(const DiagnosticLine& line) override;
80+
virtual void report(DiagnosticLine&& line) override;
81+
82+
std::vector<DiagnosticLine> lines;
83+
84+
// Prints all diagnostics to the terminal.
85+
// Not safe to use in the face of concurrent calls to report()
86+
void print(MessageSink& sink) const;
87+
// Converts this message into a string
88+
// Prefer print() if possible because it applies color
89+
// Not safe to use in the face of concurrent calls to report()
90+
std::string to_string() const;
91+
void to_string(std::string& target) const;
92+
93+
private:
94+
std::mutex m_mtx;
95+
};
96+
97+
// If T Ty is an rvalue Optional<U>, typename UnwrapOptional<Ty>::type is the type necessary to forward U
98+
// Otherwise, there is no member UnwrapOptional<Ty>::type
99+
template<class Ty>
100+
struct UnwrapOptional
101+
{
102+
// no member ::type, SFINAEs out when the input type is:
103+
// * not Optional
104+
// * not an rvalue
105+
// * volatile
106+
};
107+
108+
template<class Wrapped>
109+
struct UnwrapOptional<Optional<Wrapped>>
110+
{
111+
// prvalue
112+
using type = Wrapped;
113+
using fwd = Wrapped&&;
114+
};
115+
116+
template<class Wrapped>
117+
struct UnwrapOptional<const Optional<Wrapped>>
118+
{
119+
// const prvalue
120+
using type = Wrapped;
121+
using fwd = const Wrapped&&;
122+
};
123+
124+
template<class Wrapped>
125+
struct UnwrapOptional<Optional<Wrapped>&&>
126+
{
127+
// xvalue
128+
using type = Wrapped&&;
129+
using fwd = Wrapped&&;
130+
};
131+
132+
template<class Wrapped>
133+
struct UnwrapOptional<const Optional<Wrapped>&&>
134+
{
135+
// const xvalue
136+
using type = const Wrapped&&;
137+
using fwd = Wrapped&&;
138+
};
139+
140+
template<class Fn, class... Args>
141+
auto adapt_context_to_expected(Fn functor, Args&&... args)
142+
-> ExpectedL<typename UnwrapOptional<std::invoke_result_t<Fn, BufferedDiagnosticContext&, Args...>>::type>
143+
{
144+
using Contained = typename UnwrapOptional<std::invoke_result_t<Fn, BufferedDiagnosticContext&, Args...>>::type;
145+
BufferedDiagnosticContext bdc;
146+
auto maybe_result = functor(bdc, std::forward<Args>(args)...);
147+
if (auto result = maybe_result.get())
148+
{
149+
// N.B.: This may be a move
150+
return ExpectedL<Contained>{
151+
static_cast<
152+
typename UnwrapOptional<std::invoke_result_t<Fn, BufferedDiagnosticContext&, Args...>>::fwd>(
153+
*result),
154+
expected_left_tag};
155+
}
156+
157+
return ExpectedL<Contained>{LocalizedString::from_raw(bdc.to_string()), expected_right_tag};
158+
}
159+
}

include/vcpkg/base/parse.h

+3-4
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22

33
#include <vcpkg/base/fwd/parse.h>
44

5+
#include <vcpkg/base/diagnostics.h>
56
#include <vcpkg/base/messages.h>
67
#include <vcpkg/base/optional.h>
78
#include <vcpkg/base/stringview.h>
89
#include <vcpkg/base/unicode.h>
910

10-
#include <vcpkg/textrowcol.h>
11-
1211
#include <memory>
1312
#include <string>
1413

@@ -75,7 +74,7 @@ namespace vcpkg
7574

7675
struct ParserBase
7776
{
78-
ParserBase(StringView text, StringView origin, TextRowCol init_rowcol = {});
77+
ParserBase(StringView text, StringView origin, TextPosition init_rowcol = {1, 1});
7978

8079
static constexpr bool is_whitespace(char32_t ch) { return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'; }
8180
static constexpr bool is_lower_alpha(char32_t ch) { return ch >= 'a' && ch <= 'z'; }
@@ -131,7 +130,7 @@ namespace vcpkg
131130
Unicode::Utf8Decoder it() const { return m_it; }
132131
char32_t cur() const { return m_it == m_it.end() ? Unicode::end_of_file : *m_it; }
133132
SourceLoc cur_loc() const { return {m_it, m_start_of_line, m_row, m_column}; }
134-
TextRowCol cur_rowcol() const { return {m_row, m_column}; }
133+
TextPosition cur_rowcol() const { return {m_row, m_column}; }
135134
char32_t next();
136135
bool at_eof() const { return m_it == m_it.end(); }
137136

include/vcpkg/paragraphparser.h

+6-6
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
#include <vcpkg/fwd/paragraphparser.h>
44

5+
#include <vcpkg/base/diagnostics.h>
56
#include <vcpkg/base/expected.h>
67
#include <vcpkg/base/messages.h>
78
#include <vcpkg/base/stringview.h>
89

910
#include <vcpkg/packagespec.h>
10-
#include <vcpkg/textrowcol.h>
1111

1212
#include <map>
1313
#include <memory>
@@ -16,7 +16,7 @@
1616

1717
namespace vcpkg
1818
{
19-
using Paragraph = std::map<std::string, std::pair<std::string, TextRowCol>, std::less<>>;
19+
using Paragraph = std::map<std::string, std::pair<std::string, TextPosition>, std::less<>>;
2020

2121
struct ParagraphParser
2222
{
@@ -28,9 +28,9 @@ namespace vcpkg
2828
std::string required_field(StringLiteral fieldname);
2929

3030
std::string optional_field(StringLiteral fieldname);
31-
std::string optional_field(StringLiteral fieldname, TextRowCol& position);
31+
std::string optional_field(StringLiteral fieldname, TextPosition& position);
3232

33-
void add_error(TextRowCol position, msg::MessageT<> error_content);
33+
void add_error(TextPosition position, msg::MessageT<> error_content);
3434

3535
Optional<LocalizedString> error() const;
3636

@@ -42,8 +42,8 @@ namespace vcpkg
4242

4343
ExpectedL<std::vector<std::string>> parse_default_features_list(const std::string& str,
4444
StringView origin = "<unknown>",
45-
TextRowCol textrowcol = {});
45+
TextPosition position = {1, 1});
4646
ExpectedL<std::vector<ParsedQualifiedSpecifier>> parse_qualified_specifier_list(const std::string& str,
4747
StringView origin = "<unknown>",
48-
TextRowCol textrowcol = {});
48+
TextPosition position = {1, 1});
4949
}

include/vcpkg/sourceparagraph.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -223,5 +223,5 @@ namespace vcpkg
223223
// Exposed for testing
224224
ExpectedL<std::vector<Dependency>> parse_dependencies_list(const std::string& str,
225225
StringView origin,
226-
TextRowCol textrowcol = {});
226+
TextPosition position = {1, 1});
227227
}

include/vcpkg/textrowcol.h

-17
This file was deleted.

src/vcpkg-test/messages.cpp

+104
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include <vcpkg-test/util.h>
22

3+
#include <vcpkg/base/diagnostics.h>
34
#include <vcpkg/base/setup-messages.h>
45

56
#include <vcpkg/commands.z-generate-message-map.h>
@@ -99,3 +100,106 @@ TEST_CASE ("generate message get_format_arg_mismatches", "[messages]")
99100
CHECK(res.arguments_without_comment == std::vector<StringView>{"go", "ho"});
100101
CHECK(res.comments_without_argument == std::vector<StringView>{"blah"});
101102
}
103+
104+
namespace
105+
{
106+
struct OnlyMoveOnce
107+
{
108+
bool& m_moved;
109+
110+
explicit OnlyMoveOnce(bool& moved) : m_moved(moved) { }
111+
OnlyMoveOnce(const OnlyMoveOnce&) = delete;
112+
OnlyMoveOnce(OnlyMoveOnce&& other) : m_moved(other.m_moved)
113+
{
114+
REQUIRE(!m_moved);
115+
m_moved = true;
116+
}
117+
118+
OnlyMoveOnce& operator=(const OnlyMoveOnce&) = delete;
119+
OnlyMoveOnce& operator=(OnlyMoveOnce&&) = delete;
120+
};
121+
122+
int returns_int(DiagnosticContext&) { return 42; }
123+
std::unique_ptr<int> returns_unique_ptr(DiagnosticContext&) { return std::unique_ptr<int>{new int{42}}; }
124+
Optional<int> returns_optional_prvalue(DiagnosticContext&, int val) { return val; }
125+
const Optional<int> returns_optional_const_prvalue(DiagnosticContext&, int val) { return val; }
126+
Optional<int>&& returns_optional_xvalue(DiagnosticContext&, Optional<int>&& val) { return std::move(val); }
127+
const Optional<int>&& returns_optional_const_xvalue(DiagnosticContext&, Optional<int>&& val)
128+
{
129+
return std::move(val);
130+
}
131+
Optional<int> returns_optional_prvalue_fail(DiagnosticContext& context)
132+
{
133+
context.report(DiagnosticLine{DiagKind::Error, LocalizedString::from_raw("something bad happened")});
134+
return nullopt;
135+
}
136+
const Optional<int> returns_optional_const_prvalue_fail(DiagnosticContext& context)
137+
{
138+
context.report(DiagnosticLine{DiagKind::Error, LocalizedString::from_raw("something bad happened")});
139+
return nullopt;
140+
}
141+
Optional<int>&& returns_optional_xvalue_fail(DiagnosticContext& context, Optional<int>&& val)
142+
{
143+
val.clear();
144+
context.report(DiagnosticLine{DiagKind::Error, LocalizedString::from_raw("something bad happened")});
145+
return std::move(val);
146+
}
147+
148+
const Optional<int>&& returns_optional_const_xvalue_fail(DiagnosticContext& context, Optional<int>&& val)
149+
{
150+
val.clear();
151+
context.report(DiagnosticLine{DiagKind::Error, LocalizedString::from_raw("something bad happened")});
152+
return std::move(val);
153+
}
154+
155+
template<class Void, class Test, class... Args>
156+
constexpr bool adapt_context_to_expected_invocable_with_impl = false;
157+
158+
template<class Test, class... Args>
159+
constexpr bool adapt_context_to_expected_invocable_with_impl<
160+
std::void_t<decltype(adapt_context_to_expected(std::declval<Test>(), std::declval<Args>()...))>,
161+
Test,
162+
Args...> = true;
163+
164+
template<class Test, class... Args>
165+
constexpr bool adapt_context_to_expected_invocable_with =
166+
adapt_context_to_expected_invocable_with_impl<void, Test, Args...>;
167+
} // unnamed namespace
168+
169+
TEST_CASE ("adapt DiagnosticContext to ExpectedL", "[diagnostics]")
170+
{
171+
// adapt_context_to_expected(returns_int); // should not compile
172+
static_assert(!adapt_context_to_expected_invocable_with<decltype(returns_int)>,
173+
"Callable needs to return optional");
174+
// adapt_context_to_expected(returns_unique_ptr); // should not compile
175+
static_assert(!adapt_context_to_expected_invocable_with<decltype(returns_unique_ptr)>,
176+
"Callable needs to return optional");
177+
178+
static_assert(adapt_context_to_expected_invocable_with<decltype(returns_optional_prvalue), int>,
179+
"adapt_context_to_expected_invocable_with needs to succeed with a value that should "
180+
"work");
181+
182+
// test that the type of ExpectedL is determined correctly
183+
static_assert(std::is_same_v<ExpectedL<int>, decltype(adapt_context_to_expected(returns_optional_prvalue, 42))>,
184+
"boom");
185+
static_assert(
186+
std::is_same_v<ExpectedL<int>, decltype(adapt_context_to_expected(returns_optional_const_prvalue, 42))>,
187+
"boom");
188+
static_assert(
189+
std::is_same_v<ExpectedL<int&&>,
190+
decltype(adapt_context_to_expected(returns_optional_xvalue, std::declval<Optional<int>>()))>,
191+
"boom");
192+
static_assert(std::is_same_v<ExpectedL<const int&&>,
193+
decltype(adapt_context_to_expected(returns_optional_const_xvalue,
194+
std::declval<Optional<int>>()))>,
195+
"boom");
196+
{
197+
auto adapted = adapt_context_to_expected(returns_optional_prvalue, 42);
198+
REQUIRE(adapted.value_or_exit(VCPKG_LINE_INFO) == 42);
199+
}
200+
{
201+
auto adapted = adapt_context_to_expected(returns_optional_prvalue_fail);
202+
REQUIRE(!adapted.has_value());
203+
REQUIRE(adapted.error() == LocalizedString::from_raw("error: something bad happened"));
204+
}
205+
}

src/vcpkg-test/paragraph.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace
1515
{
1616
pghs.emplace_back();
1717
for (auto&& kv : p)
18-
pghs.back().emplace(kv.first, std::make_pair(kv.second, vcpkg::TextRowCol{}));
18+
pghs.back().emplace(kv.first, std::make_pair(kv.second, vcpkg::TextPosition{}));
1919
}
2020
return vcpkg::SourceControlFile::parse_control_file("", std::move(pghs));
2121
}
@@ -24,7 +24,7 @@ namespace
2424
{
2525
Paragraph pgh;
2626
for (auto&& kv : v)
27-
pgh.emplace(kv.first, std::make_pair(kv.second, vcpkg::TextRowCol{}));
27+
pgh.emplace(kv.first, std::make_pair(kv.second, vcpkg::TextPosition{}));
2828

2929
return vcpkg::BinaryParagraph("test", std::move(pgh));
3030
}

0 commit comments

Comments
 (0)