Porytiles
Loading...
Searching...
No Matches
header_enum_map_provider.cpp
Go to the documentation of this file.
2
3#include <bit>
4#include <utility>
5
8
9namespace porytiles {
10
11namespace {
12
14[[nodiscard]] FormattableError make_duplicate_error(
15 const std::string &header_message,
16 SourcePosition duplicate_pos,
17 const std::string &note_message,
18 SourcePosition original_pos,
19 const std::vector<std::string> &file_lines,
20 const TextFormatter *format)
21{
22 std::vector<std::string> lines;
23 FileHighlightPrinter printer{format};
24
25 // Add header for duplicate location
26 lines.push_back(header_message);
27
28 // Add source context for duplicate location (convert 1-based to 0-based)
29 assert_or_panic(duplicate_pos.line > 0, "duplicate_pos.line must be positive (1-based)");
30 assert_or_panic(duplicate_pos.line <= file_lines.size(), "duplicate_pos.line exceeds file bounds");
31 assert_or_panic(duplicate_pos.column > 0, "duplicate_pos.column must be positive (1-based)");
32 auto dup_context = printer.print(file_lines, duplicate_pos.line - 1, duplicate_pos.column - 1);
33 for (auto &line : dup_context) {
34 lines.push_back(std::move(line));
35 }
36
37 // Add blank line separator
38 lines.emplace_back("");
39
40 // Add note about original location
41 lines.push_back(note_message);
42
43 // Add source context for original location (convert 1-based to 0-based)
44 assert_or_panic(original_pos.line > 0, "original_pos.line must be positive (1-based)");
45 assert_or_panic(original_pos.line <= file_lines.size(), "original_pos.line exceeds file bounds");
46 assert_or_panic(original_pos.column > 0, "original_pos.column must be positive (1-based)");
47 auto orig_context = printer.print(file_lines, original_pos.line - 1, original_pos.column - 1);
48 for (auto &line : orig_context) {
49 lines.push_back(std::move(line));
50 }
51
52 return FormattableError{std::move(lines)};
53}
54
55} // namespace
56
57template <typename Entry>
58ChainableResult<void> HeaderEnumMapProvider::try_add_entry(const Entry &entry) const
59{
60 const auto &name = entry.name();
61
62 // Filter: must start with the configured prefix
63 if (!name.starts_with(definition_.prefix)) {
64 return {};
65 }
66
67 // Filter: skip names the definition excludes
68 if (definition_.skipped.contains(name)) {
69 return {};
70 }
71
72 auto raw_value = entry.int_value();
73 const auto &new_pos = entry.position();
74
75 // A parsed value outside the field's range is a hard error, not a name to drop quietly. Dropping it would resurface
76 // later as a baffling "no such name" lookup failure; the real problem is that the field's mask is too narrow for
77 // the header's constants (or the name is a sentinel that belongs in the skipped set).
78 if (raw_value < 0 || raw_value > definition_.max_value) {
79 load_failed_ = true;
80 std::vector<std::string> lines;
81 FileHighlightPrinter printer{format_};
82
83 lines.push_back(format_->format(
84 "{}:{}:{}: '{}' has value '{}', which does not fit in the {}-bit field '{}'.",
85 FormatParam{header_path_, Style::bold},
86 new_pos.line,
87 new_pos.column,
88 FormatParam{name, Style::bold},
89 FormatParam{raw_value, Style::bold},
90 FormatParam{std::bit_width(definition_.max_value)},
91 FormatParam{definition_.field_display_name, Style::bold}));
92
93 assert_or_panic(new_pos.line > 0, "new_pos.line must be positive (1-based)");
94 assert_or_panic(new_pos.line <= driver_->file_lines().size(), "new_pos.line exceeds file bounds");
95 assert_or_panic(new_pos.column > 0, "new_pos.column must be positive (1-based)");
96 auto context = printer.print(driver_->file_lines(), new_pos.line - 1, new_pos.column - 1);
97 for (auto &line : context) {
98 lines.push_back(std::move(line));
99 }
100
101 lines.emplace_back("");
102 lines.push_back(format_->format(
103 "{} widen the field's mask to cover this value, or add '{}' to the provider's skipped names to ignore it.",
104 FormatParam{"note:", Style::cyan | Style::bold},
105 FormatParam{name, Style::bold}));
106
107 return FormattableError{std::move(lines)};
108 }
109
110 auto value = static_cast<std::uint32_t>(raw_value);
111
112 // Check for duplicate name
113 if (name_to_value_.contains(name)) {
114 load_failed_ = true;
115 const auto &orig_pos = name_to_position_.at(name);
116 return make_duplicate_error(
117 format_->format(
118 "{}:{}:{}: duplicate {} name '{}'.",
119 FormatParam{header_path_, Style::bold},
120 new_pos.line,
121 new_pos.column,
122 FormatParam{definition_.field_display_name},
123 FormatParam{name, Style::bold}),
124 new_pos,
125 format_->format(
126 "{} originally defined at line {}:", FormatParam{"note:", Style::cyan | Style::bold}, orig_pos.line),
127 orig_pos,
128 driver_->file_lines(),
129 format_);
130 }
131
132 // Check for duplicate value
133 if (value_to_name_.contains(value)) {
134 load_failed_ = true;
135 const auto &orig_name = value_to_name_.at(value);
136 const auto &orig_pos = value_to_position_.at(value);
137 return make_duplicate_error(
138 format_->format(
139 "{}:{}:{}: duplicate {} value '{}': both '{}' and '{}' have this value.",
140 FormatParam{header_path_, Style::bold},
141 new_pos.line,
142 new_pos.column,
143 FormatParam{definition_.field_display_name},
144 FormatParam{value, Style::bold},
145 FormatParam{orig_name, Style::bold},
146 FormatParam{name, Style::bold}),
147 new_pos,
148 format_->format(
149 "{} '{}' originally defined at line {}:",
150 FormatParam{"note:", Style::cyan | Style::bold},
151 FormatParam{orig_name, Style::bold},
152 orig_pos.line),
153 orig_pos,
154 driver_->file_lines(),
155 format_);
156 }
157
158 // Insert into all maps
159 name_to_value_[name] = value;
160 value_to_name_[value] = name;
161 name_to_position_[name] = new_pos;
162 value_to_position_[value] = new_pos;
163
164 return {};
165}
166
168{
169 if (!name.starts_with(definition_.prefix)) {
170 return FormattableError{
171 "Invalid {} name '{}': expected prefix '{}'.",
172 FormatParam{definition_.field_display_name},
174 FormatParam{definition_.prefix, Style::bold}};
175 }
176
177 auto load_result = ensure_loaded();
178 if (!load_result.has_value()) {
181 "Provider lookup for field '{}' failed.", FormatParam{definition_.field_display_name, Style::bold}},
182 load_result};
183 }
184
185 const auto it = name_to_value_.find(name);
186 if (it == name_to_value_.end()) {
187 return FormattableError{
188 "No {} named '{}' exists in '{}'.",
189 FormatParam{definition_.field_display_name},
191 FormatParam{header_path_.string(), Style::bold}};
192 }
193 return it->second;
194}
195
197{
198 auto load_result = ensure_loaded();
199 if (!load_result.has_value()) {
202 "Provider lookup for field '{}' failed.", FormatParam{definition_.field_display_name, Style::bold}},
203 load_result};
204 }
205
206 const auto it = value_to_name_.find(value);
207 if (it == value_to_name_.end()) {
208 return FormattableError{
209 "No {} with value '{}' exists in '{}'.",
210 FormatParam{definition_.field_display_name},
211 FormatParam{value, Style::bold},
212 FormatParam{header_path_.string(), Style::bold}};
213 }
214 return it->second;
215}
216
217ChainableResult<void> HeaderEnumMapProvider::ensure_loaded() const
218{
219 if (loaded_) {
220 if (load_failed_) {
221 return FormattableError{
222 "Header file for field '{}' previously failed to load.",
224 }
225 return {};
226 }
227
228 loaded_ = true;
229
230 // Create and store CParserFacade for rich error formatting with source context
231 driver_ = std::make_unique<CParserFacade>(header_path_, format_);
232
233 // Parse #define statements when the format admits them
234 if (definition_.format == HeaderFormat::defines_only || definition_.format == HeaderFormat::either) {
235 auto defines_result = driver_->parse_defines();
236 if (!defines_result.has_value()) {
237 load_failed_ = true;
238 return ChainableResult<void>{defines_result};
239 }
240 for (const auto &def : defines_result.value()) {
241 if (!def.has_int_value()) {
242 continue;
243 }
244 auto insert_result = try_add_entry(def);
245 if (!insert_result.has_value()) {
246 return insert_result;
247 }
248 }
249 }
250
251 // Parse enum declarations when the format admits them
252 if (definition_.format == HeaderFormat::enums_only || definition_.format == HeaderFormat::either) {
253 auto enums_result = driver_->parse_enums();
254 if (!enums_result.has_value()) {
255 load_failed_ = true;
256 return ChainableResult<void>{enums_result};
257 }
258 for (const auto &enum_decl : enums_result.value()) {
259 for (const auto &member : enum_decl.members()) {
260 auto insert_result = try_add_entry(member);
261 if (!insert_result.has_value()) {
262 return insert_result;
263 }
264 }
265 }
266 }
267
268 if (name_to_value_.empty()) {
269 load_failed_ = true;
270 return FormattableError{
271 "Field '{}' declared provider prefix '{}' in '{}' but no matching names were found.",
272 FormatParam{definition_.field_display_name, Style::bold},
273 FormatParam{definition_.prefix, Style::bold},
274 FormatParam{header_path_.string(), Style::bold}};
275 }
276
277 return {};
278}
279
281 const std::filesystem::path &project_root,
282 const Schema &schema,
283 gsl::not_null<const TextFormatter *> format,
284 gsl::not_null<const UserDiagnostics *> diag)
285{
286 // Membership contract: the map contains exactly the schema's has_provider() fields, so downstream code
287 // can key raw-vs-provider handling off has_provider() and treat a missing map entry as an internal bug.
288 ProviderMap providers{};
289 for (const Field &field : schema.fields()) {
290 if (!field.has_provider()) {
291 continue;
292 }
293 const ProviderDefinition &definition = field.provider_definition();
294 providers.emplace(
295 field.name(),
296 std::make_unique<HeaderEnumMapProvider>(
297 project_root / definition.header,
298 definition.to_enum_definition(field.name(), field.max_value()),
299 format,
300 diag));
301 }
302 return providers;
303}
304
305} // namespace porytiles
A result type that maintains a chainable sequence of errors for debugging and error reporting.
One named bit-field within a metatile attribute layout.
A text parameter with associated styling for formatted output.
General-purpose error implementation with formatted message support.
Definition error.hpp:57
ChainableResult< std::uint32_t > lookup(const std::string &name) const override
Looks up the numeric value for a constant name.
A validated metatile attribute layout: an ordered set of non-overlapping fields.
const std::vector< Field > & fields() const
static const Style bold
Bold text formatting.
virtual std::string format(const std::string &format_str, const std::vector< FormatParam > &params) const
Formats a string with styled parameters using fmtlib syntax.
std::string name
void assert_or_panic(bool condition, const StringViewSourceLoc &s)
Conditionally panics if the given condition is false.
Definition panic.cpp:53
std::map< std::string, std::unique_ptr< EnumMapProvider >, std::less<> > ProviderMap
Maps schema field names to the provider that names that field's values.
ProviderMap build_provider_map(const std::filesystem::path &project_root, const Schema &schema, gsl::not_null< const TextFormatter * > format, gsl::not_null< const UserDiagnostics * > diag)
Builds a header provider for every provider-backed field in a schema.
std::unordered_set< std::string > skipped
Describes where and how a field's named values are declared.
EnumDefinition to_enum_definition(std::string field_display_name, std::uint32_t max_value) const
Builds the resolvable enum definition a header provider needs from this description.