]>
Commit | Line | Data |
---|---|---|
767698ff | 1 | // Copyright (C) 2020-2024 Free Software Foundation, Inc. |
2327631e AC |
2 | |
3 | // This file is part of GCC. | |
4 | ||
5 | // GCC is free software; you can redistribute it and/or modify it under | |
6 | // the terms of the GNU General Public License as published by the Free | |
7 | // Software Foundation; either version 3, or (at your option) any later | |
8 | // version. | |
9 | ||
10 | // GCC is distributed in the hope that it will be useful, but WITHOUT ANY | |
11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
12 | // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
13 | // for more details. | |
14 | ||
15 | // You should have received a copy of the GNU General Public License | |
16 | // along with GCC; see the file COPYING3. If not see | |
17 | // <http://www.gnu.org/licenses/>. | |
18 | ||
79d8fb09 AC |
19 | #include "expected.h" |
20 | #include "rust-ast.h" | |
21 | #include "rust-diagnostics.h" | |
2327631e AC |
22 | #include "rust-forever-stack.h" |
23 | #include "rust-rib.h" | |
24 | #include "optional.h" | |
25 | ||
26 | namespace Rust { | |
27 | namespace Resolver2_0 { | |
28 | ||
29 | template <Namespace N> | |
30 | bool | |
31 | ForeverStack<N>::Node::is_root () const | |
32 | { | |
33 | return !parent.has_value (); | |
34 | } | |
35 | ||
36 | template <Namespace N> | |
37 | bool | |
38 | ForeverStack<N>::Node::is_leaf () const | |
39 | { | |
40 | return children.empty (); | |
41 | } | |
42 | ||
43 | template <Namespace N> | |
44 | void | |
45 | ForeverStack<N>::Node::insert_child (Link link, Node child) | |
46 | { | |
47 | auto res = children.insert ({link, child}); | |
48 | ||
49 | // Do we want to error if the child already exists? Probably not, right? | |
50 | // That's kinda the point, isn't it. So this method always succeeds, right? | |
51 | } | |
52 | ||
2327631e AC |
53 | template <Namespace N> |
54 | void | |
55 | ForeverStack<N>::push (Rib rib, NodeId id, tl::optional<Identifier> path) | |
56 | { | |
57 | push_inner (rib, Link (id, path)); | |
58 | } | |
59 | ||
60 | template <Namespace N> | |
61 | void | |
62 | ForeverStack<N>::push_inner (Rib rib, Link link) | |
63 | { | |
64 | // If the link does not exist, we create it and emplace a new `Node` with the | |
65 | // current node as its parent. `unordered_map::emplace` returns a pair with | |
66 | // the iterator and a boolean. If the value already exists, the iterator | |
67 | // points to it. Otherwise, it points to the newly emplaced value, so we can | |
68 | // just update our cursor(). | |
12b2dcb0 AC |
69 | auto emplace = cursor ().children.emplace ( |
70 | std::make_pair (link, Node (rib, link.id, cursor ()))); | |
2327631e AC |
71 | |
72 | auto it = emplace.first; | |
73 | auto existed = !emplace.second; | |
74 | ||
75 | rust_debug ("inserting link: Link(%d [%s]): existed? %s", link.id, | |
76 | link.path.has_value () ? link.path.value ().as_string ().c_str () | |
77 | : "<anon>", | |
78 | existed ? "yes" : "no"); | |
79 | ||
80 | // We update the cursor | |
81 | update_cursor (it->second); | |
82 | } | |
83 | ||
84 | template <Namespace N> | |
85 | void | |
86 | ForeverStack<N>::pop () | |
87 | { | |
6db677ba | 88 | rust_assert (!cursor ().is_root ()); |
2327631e AC |
89 | |
90 | rust_debug ("popping link"); | |
91 | ||
92 | for (const auto &kv : cursor ().rib.get_values ()) | |
93 | rust_debug ("current_rib: k: %s, v: %d", kv.first.c_str (), kv.second); | |
94 | ||
95 | if (cursor ().parent.has_value ()) | |
96 | for (const auto &kv : cursor ().parent.value ().rib.get_values ()) | |
97 | rust_debug ("new cursor: k: %s, v: %d", kv.first.c_str (), kv.second); | |
98 | ||
99 | update_cursor (cursor ().parent.value ()); | |
100 | } | |
101 | ||
79d8fb09 AC |
102 | static tl::expected<NodeId, DuplicateNameError> |
103 | insert_inner (Rib &rib, std::string name, NodeId node, bool can_shadow) | |
104 | { | |
105 | return rib.insert (name, node, can_shadow); | |
106 | } | |
107 | ||
2327631e AC |
108 | template <Namespace N> |
109 | tl::expected<NodeId, DuplicateNameError> | |
110 | ForeverStack<N>::insert (Identifier name, NodeId node) | |
111 | { | |
112 | auto &innermost_rib = peek (); | |
113 | ||
114 | // So what do we do here - if the Rib has already been pushed in an earlier | |
115 | // pass, we might end up in a situation where it is okay to re-add new names. | |
116 | // Do we just ignore that here? Do we keep track of if the Rib is new or not? | |
117 | // should our cursor have info on the current node like "is it newly pushed"? | |
79d8fb09 AC |
118 | return insert_inner (innermost_rib, name.as_string (), node, false); |
119 | } | |
120 | ||
121 | template <Namespace N> | |
122 | tl::expected<NodeId, DuplicateNameError> | |
123 | ForeverStack<N>::insert_at_root (Identifier name, NodeId node) | |
124 | { | |
125 | auto &root_rib = root.rib; | |
126 | ||
127 | // inserting in the root of the crate is never a shadowing operation, even for | |
128 | // macros | |
129 | return insert_inner (root_rib, name.as_string (), node, false); | |
130 | } | |
131 | ||
132 | // Specialization for Macros and Labels - where we are allowed to shadow | |
133 | // existing definitions | |
134 | template <> | |
135 | inline tl::expected<NodeId, DuplicateNameError> | |
136 | ForeverStack<Namespace::Macros>::insert (Identifier name, NodeId node) | |
137 | { | |
138 | return insert_inner (peek (), name.as_string (), node, true); | |
139 | } | |
140 | ||
141 | template <> | |
142 | inline tl::expected<NodeId, DuplicateNameError> | |
143 | ForeverStack<Namespace::Labels>::insert (Identifier name, NodeId node) | |
144 | { | |
145 | return insert_inner (peek (), name.as_string (), node, true); | |
2327631e AC |
146 | } |
147 | ||
148 | template <Namespace N> | |
149 | Rib & | |
150 | ForeverStack<N>::peek () | |
151 | { | |
152 | return cursor ().rib; | |
153 | } | |
154 | ||
155 | template <Namespace N> | |
156 | const Rib & | |
157 | ForeverStack<N>::peek () const | |
158 | { | |
159 | return cursor ().rib; | |
160 | } | |
161 | ||
162 | template <Namespace N> | |
163 | void | |
79d8fb09 | 164 | ForeverStack<N>::reverse_iter (std::function<KeepGoing (Node &)> lambda) |
2327631e | 165 | { |
79d8fb09 AC |
166 | return reverse_iter (cursor (), lambda); |
167 | } | |
168 | ||
169 | template <Namespace N> | |
170 | void | |
171 | ForeverStack<N>::reverse_iter (Node &start, | |
172 | std::function<KeepGoing (Node &)> lambda) | |
173 | { | |
174 | auto *tmp = &start; | |
2327631e AC |
175 | |
176 | while (true) | |
177 | { | |
79d8fb09 | 178 | auto keep_going = lambda (*tmp); |
2327631e AC |
179 | if (keep_going == KeepGoing::No) |
180 | return; | |
181 | ||
79d8fb09 | 182 | if (tmp->is_root ()) |
2327631e AC |
183 | return; |
184 | ||
79d8fb09 | 185 | tmp = &tmp->parent.value (); |
2327631e AC |
186 | } |
187 | } | |
188 | ||
189 | template <Namespace N> | |
190 | typename ForeverStack<N>::Node & | |
191 | ForeverStack<N>::cursor () | |
192 | { | |
193 | return cursor_reference; | |
194 | } | |
195 | ||
196 | template <Namespace N> | |
197 | const typename ForeverStack<N>::Node & | |
198 | ForeverStack<N>::cursor () const | |
199 | { | |
200 | return cursor_reference; | |
201 | } | |
202 | ||
203 | template <Namespace N> | |
204 | void | |
205 | ForeverStack<N>::update_cursor (Node &new_cursor) | |
206 | { | |
207 | cursor_reference = new_cursor; | |
208 | } | |
209 | ||
210 | template <Namespace N> | |
eec00ae2 | 211 | tl::optional<NodeId> |
446ab9b2 | 212 | ForeverStack<N>::get (const Identifier &name) |
79d8fb09 AC |
213 | { |
214 | tl::optional<NodeId> resolved_node = tl::nullopt; | |
215 | ||
216 | // TODO: Can we improve the API? have `reverse_iter` return an optional? | |
217 | reverse_iter ([&resolved_node, &name] (Node ¤t) { | |
218 | auto candidate = current.rib.get (name.as_string ()); | |
219 | ||
220 | return candidate.map_or ( | |
221 | [&resolved_node] (NodeId found) { | |
446ab9b2 AC |
222 | // for most namespaces, we do not need to care about various ribs - they |
223 | // are available from all contexts if defined in the current scope, or | |
224 | // an outermore one. so if we do have a candidate, we can return it | |
79d8fb09 AC |
225 | // directly and stop iterating |
226 | resolved_node = found; | |
227 | ||
228 | return KeepGoing::No; | |
229 | }, | |
230 | // if there was no candidate, we keep iterating | |
231 | KeepGoing::Yes); | |
232 | }); | |
233 | ||
234 | return resolved_node; | |
235 | } | |
236 | ||
eec00ae2 AC |
237 | template <> |
238 | tl::optional<NodeId> inline ForeverStack<Namespace::Labels>::get ( | |
239 | const Identifier &name) | |
240 | { | |
241 | tl::optional<NodeId> resolved_node = tl::nullopt; | |
242 | ||
243 | reverse_iter ([&resolved_node, &name] (Node ¤t) { | |
244 | // looking up for labels cannot go through function ribs | |
245 | // TODO: What other ribs? | |
246 | if (current.rib.kind == Rib::Kind::Function) | |
247 | return KeepGoing::No; | |
248 | ||
249 | auto candidate = current.rib.get (name.as_string ()); | |
250 | ||
251 | // FIXME: Factor this in a function with the generic `get` | |
252 | return candidate.map_or ( | |
253 | [&resolved_node] (NodeId found) { | |
254 | resolved_node = found; | |
255 | ||
256 | return KeepGoing::No; | |
257 | }, | |
258 | KeepGoing::Yes); | |
259 | }); | |
260 | ||
261 | return resolved_node; | |
262 | } | |
263 | ||
79d8fb09 AC |
264 | /* Check if an iterator points to the last element */ |
265 | template <typename I, typename C> | |
266 | static bool | |
267 | is_last (const I &iterator, const C &collection) | |
268 | { | |
269 | return iterator + 1 == collection.end (); | |
270 | } | |
271 | ||
272 | /* Check if an iterator points to the start of the collection */ | |
273 | template <typename I, typename C> | |
274 | static bool | |
275 | is_start (const I &iterator, const C &collection) | |
276 | { | |
277 | return iterator == collection.begin (); | |
278 | } | |
279 | ||
280 | template <Namespace N> | |
281 | typename ForeverStack<N>::Node & | |
282 | ForeverStack<N>::find_closest_module (Node &starting_point) | |
283 | { | |
284 | auto *closest_module = &starting_point; | |
285 | ||
286 | reverse_iter (starting_point, [&closest_module] (Node ¤t) { | |
287 | if (current.rib.kind == Rib::Kind::Module || current.is_root ()) | |
288 | { | |
289 | closest_module = ¤t; | |
290 | return KeepGoing::No; | |
291 | } | |
292 | ||
293 | return KeepGoing::Yes; | |
294 | }); | |
295 | ||
296 | return *closest_module; | |
297 | } | |
298 | ||
299 | /* If a the given condition is met, emit an error about misused leading path | |
300 | * segments */ | |
446ab9b2 | 301 | template <typename S> |
79d8fb09 | 302 | static inline bool |
446ab9b2 | 303 | check_leading_kw_at_start (const S &segment, bool condition) |
79d8fb09 AC |
304 | { |
305 | if (condition) | |
306 | rust_error_at ( | |
307 | segment.get_locus (), ErrorCode::E0433, | |
308 | "leading path segment %qs can only be used at the beginning of a path", | |
309 | segment.as_string ().c_str ()); | |
310 | ||
311 | return condition; | |
312 | } | |
313 | ||
314 | // we first need to handle the "starting" segments - `super`, `self` or | |
315 | // `crate`. we don't need to do anything for `self` and can just skip it. for | |
316 | // `crate`, we need to go back to the root of the current stack. for each | |
317 | // `super` segment, we go back to the cursor's parent until we reach the | |
318 | // correct one or the root. | |
319 | template <Namespace N> | |
446ab9b2 AC |
320 | template <typename S> |
321 | tl::optional<typename std::vector<S>::const_iterator> | |
322 | ForeverStack<N>::find_starting_point (const std::vector<S> &segments, | |
323 | Node &starting_point) | |
79d8fb09 AC |
324 | { |
325 | auto iterator = segments.begin (); | |
326 | ||
327 | // If we need to do path segment resolution, then we start | |
328 | // at the closest module. In order to resolve something like `foo::bar!()`, we | |
329 | // need to get back to the surrounding module, and look for a child module | |
330 | // named `foo`. | |
331 | if (segments.size () > 1) | |
332 | starting_point = find_closest_module (starting_point); | |
333 | ||
334 | for (; !is_last (iterator, segments); iterator++) | |
335 | { | |
66e8a161 | 336 | auto &seg = *iterator; |
c5925f34 AC |
337 | auto is_self_or_crate |
338 | = seg.is_crate_path_seg () || seg.is_lower_self_seg (); | |
79d8fb09 AC |
339 | |
340 | // if we're after the first path segment and meet `self` or `crate`, it's | |
341 | // an error - we should only be seeing `super` keywords at this point | |
342 | if (check_leading_kw_at_start (seg, !is_start (iterator, segments) | |
343 | && is_self_or_crate)) | |
344 | return tl::nullopt; | |
345 | ||
346 | if (seg.is_crate_path_seg ()) | |
347 | { | |
348 | starting_point = root; | |
349 | iterator++; | |
350 | break; | |
351 | } | |
c5925f34 | 352 | if (seg.is_lower_self_seg ()) |
79d8fb09 AC |
353 | { |
354 | // do nothing and exit | |
355 | iterator++; | |
356 | break; | |
357 | } | |
358 | if (seg.is_super_path_seg ()) | |
359 | { | |
360 | if (starting_point.is_root ()) | |
361 | { | |
362 | rust_error_at (seg.get_locus (), ErrorCode::E0433, | |
363 | "too many leading %<super%> keywords"); | |
364 | return tl::nullopt; | |
365 | } | |
366 | ||
367 | starting_point = find_closest_module (starting_point.parent.value ()); | |
368 | continue; | |
369 | } | |
370 | ||
371 | // now we've gone through the allowed `crate`, `self` or leading `super` | |
372 | // segments. we can start resolving each segment itself. | |
373 | // if we do see another leading segment, then we can error out. | |
374 | break; | |
375 | } | |
376 | ||
377 | return iterator; | |
378 | } | |
379 | ||
380 | template <Namespace N> | |
446ab9b2 | 381 | template <typename S> |
79d8fb09 AC |
382 | tl::optional<typename ForeverStack<N>::Node &> |
383 | ForeverStack<N>::resolve_segments ( | |
446ab9b2 AC |
384 | Node &starting_point, const std::vector<S> &segments, |
385 | typename std::vector<S>::const_iterator iterator) | |
79d8fb09 AC |
386 | { |
387 | auto *current_node = &starting_point; | |
388 | for (; !is_last (iterator, segments); iterator++) | |
389 | { | |
390 | auto &seg = *iterator; | |
391 | auto str = seg.as_string (); | |
392 | rust_debug ("[ARTHUR]: resolving segment part: %s", str.c_str ()); | |
393 | ||
394 | // check that we don't encounter *any* leading keywords afterwards | |
395 | if (check_leading_kw_at_start (seg, seg.is_crate_path_seg () | |
396 | || seg.is_super_path_seg () | |
c5925f34 | 397 | || seg.is_lower_self_seg ())) |
79d8fb09 AC |
398 | return tl::nullopt; |
399 | ||
400 | tl::optional<typename ForeverStack<N>::Node &> child = tl::nullopt; | |
401 | ||
402 | for (auto &kv : current_node->children) | |
403 | { | |
404 | auto &link = kv.first; | |
405 | ||
406 | if (link.path.map_or ( | |
407 | [&str] (Identifier path) { | |
408 | auto &path_str = path.as_string (); | |
409 | return str == path_str; | |
410 | }, | |
411 | false)) | |
412 | { | |
413 | child = kv.second; | |
414 | break; | |
415 | } | |
416 | } | |
417 | ||
418 | if (!child.has_value ()) | |
419 | { | |
420 | rust_error_at (seg.get_locus (), ErrorCode::E0433, | |
421 | "failed to resolve path segment %qs", str.c_str ()); | |
422 | return tl::nullopt; | |
423 | } | |
424 | ||
425 | current_node = &child.value (); | |
426 | } | |
427 | ||
428 | return *current_node; | |
429 | } | |
430 | ||
431 | template <Namespace N> | |
5fd3de11 | 432 | template <typename S> |
79d8fb09 | 433 | tl::optional<NodeId> |
5fd3de11 | 434 | ForeverStack<N>::resolve_path (const std::vector<S> &segments) |
79d8fb09 | 435 | { |
5fd3de11 AC |
436 | // TODO: What to do if segments.empty() ? |
437 | ||
446ab9b2 | 438 | // if there's only one segment, we just use `get` |
5fd3de11 AC |
439 | if (segments.size () == 1) |
440 | return get (segments.back ().as_string ()); | |
446ab9b2 | 441 | |
79d8fb09 | 442 | auto starting_point = cursor (); |
79d8fb09 AC |
443 | |
444 | return find_starting_point (segments, starting_point) | |
445 | .and_then ([this, &segments, &starting_point] ( | |
5fd3de11 | 446 | typename std::vector<S>::const_iterator iterator) { |
79d8fb09 AC |
447 | return resolve_segments (starting_point, segments, iterator); |
448 | }) | |
5fd3de11 AC |
449 | .and_then ([&segments] (Node final_node) { |
450 | return final_node.rib.get (segments.back ().as_string ()); | |
79d8fb09 AC |
451 | }); |
452 | } | |
2327631e | 453 | |
12b2dcb0 AC |
454 | template <Namespace N> |
455 | tl::optional<std::pair<typename ForeverStack<N>::Node &, std::string>> | |
456 | ForeverStack<N>::dfs (ForeverStack<N>::Node &starting_point, NodeId to_find) | |
457 | { | |
458 | auto &values = starting_point.rib.get_values (); | |
459 | ||
460 | for (auto &kv : values) | |
461 | if (kv.second == to_find) | |
462 | return {{starting_point, kv.first}}; | |
463 | ||
464 | for (auto &child : starting_point.children) | |
465 | { | |
466 | auto candidate = dfs (child.second, to_find); | |
467 | ||
468 | if (candidate.has_value ()) | |
469 | return candidate; | |
470 | } | |
471 | ||
472 | return tl::nullopt; | |
473 | } | |
474 | ||
475 | template <Namespace N> | |
476 | tl::optional<Resolver::CanonicalPath> | |
477 | ForeverStack<N>::to_canonical_path (NodeId id) | |
478 | { | |
479 | // find the id in the current forever stack, starting from the root, | |
480 | // performing either a BFS or DFS once the Node containing the ID is found, go | |
481 | // back up to the root (parent().parent().parent()...) accumulate link | |
482 | // segments reverse them that's your canonical path | |
483 | ||
484 | return dfs (root, id).map ([this, id] (std::pair<Node &, std::string> tuple) { | |
485 | auto containing_node = tuple.first; | |
486 | auto name = tuple.second; | |
487 | ||
488 | auto segments = std::vector<Resolver::CanonicalPath> (); | |
489 | ||
490 | reverse_iter (containing_node, [&segments] (Node ¤t) { | |
491 | if (current.is_root ()) | |
492 | return KeepGoing::No; | |
493 | ||
494 | auto children = current.parent.value ().children; | |
495 | const Link *outer_link = nullptr; | |
496 | ||
497 | for (auto &kv : children) | |
498 | { | |
499 | auto &link = kv.first; | |
500 | auto &child = kv.second; | |
501 | ||
502 | if (link.id == child.id) | |
503 | { | |
504 | outer_link = &link; | |
505 | break; | |
506 | } | |
507 | } | |
508 | ||
509 | rust_assert (outer_link); | |
510 | ||
511 | outer_link->path.map ([&segments, outer_link] (Identifier path) { | |
512 | segments.emplace (segments.begin (), | |
513 | Resolver::CanonicalPath::new_seg (outer_link->id, | |
514 | path.as_string ())); | |
515 | }); | |
516 | ||
517 | return KeepGoing::Yes; | |
518 | }); | |
519 | ||
520 | auto path = Resolver::CanonicalPath::create_empty (); | |
521 | for (const auto &segment : segments) | |
522 | path = path.append (segment); | |
523 | ||
524 | // Finally, append the name | |
525 | path = path.append (Resolver::CanonicalPath::new_seg (id, name)); | |
12b2dcb0 AC |
526 | |
527 | return path; | |
528 | }); | |
529 | } | |
530 | ||
531 | template <Namespace N> | |
532 | tl::optional<Rib &> | |
232f94af | 533 | ForeverStack<N>::dfs_rib (ForeverStack<N>::Node &starting_point, NodeId to_find) |
12b2dcb0 | 534 | { |
232f94af AC |
535 | if (starting_point.id == to_find) |
536 | return starting_point.rib; | |
537 | ||
538 | for (auto &child : starting_point.children) | |
539 | { | |
540 | auto candidate = dfs_rib (child.second, to_find); | |
541 | ||
542 | if (candidate.has_value ()) | |
543 | return candidate; | |
544 | } | |
545 | ||
12b2dcb0 AC |
546 | return tl::nullopt; |
547 | } | |
548 | ||
232f94af AC |
549 | template <Namespace N> |
550 | tl::optional<Rib &> | |
551 | ForeverStack<N>::to_rib (NodeId rib_id) | |
552 | { | |
553 | return dfs_rib (root, rib_id); | |
554 | } | |
555 | ||
2327631e AC |
556 | template <Namespace N> |
557 | void | |
558 | ForeverStack<N>::stream_rib (std::stringstream &stream, const Rib &rib, | |
559 | const std::string &next, | |
560 | const std::string &next_next) | |
561 | { | |
562 | if (rib.get_values ().empty ()) | |
563 | { | |
564 | stream << next << "rib: {},\n"; | |
565 | return; | |
566 | } | |
567 | ||
568 | stream << next << "rib: {\n"; | |
569 | ||
570 | for (const auto &kv : rib.get_values ()) | |
571 | stream << next_next << kv.first << ": " << kv.second << "\n"; | |
572 | ||
573 | stream << next << "},\n"; | |
574 | } | |
575 | ||
576 | template <Namespace N> | |
577 | void | |
578 | ForeverStack<N>::stream_node (std::stringstream &stream, unsigned indentation, | |
579 | const ForeverStack<N>::Node &node) | |
580 | { | |
581 | auto indent = std::string (indentation, ' '); | |
582 | auto next = std::string (indentation + 4, ' '); | |
583 | auto next_next = std::string (indentation + 8, ' '); | |
584 | ||
585 | stream << indent << "Node {\n" | |
586 | << next << "is_root: " << (node.is_root () ? "true" : "false") << ",\n" | |
587 | << next << "is_leaf: " << (node.is_leaf () ? "true" : "false") | |
588 | << ",\n"; | |
589 | ||
590 | stream_rib (stream, node.rib, next, next_next); | |
591 | ||
592 | stream << indent << "}\n"; | |
593 | ||
594 | for (auto &kv : node.children) | |
595 | { | |
596 | auto link = kv.first; | |
597 | auto child = kv.second; | |
598 | stream << indent << "Link (" << link.id << ", " | |
599 | << (link.path.has_value () ? link.path.value ().as_string () | |
600 | : "<anon>") | |
601 | << "):\n"; | |
602 | ||
603 | stream_node (stream, indentation + 4, child); | |
604 | ||
605 | stream << '\n'; | |
606 | } | |
607 | } | |
608 | ||
609 | template <Namespace N> | |
610 | std::string | |
611 | ForeverStack<N>::as_debug_string () | |
612 | { | |
613 | std::stringstream stream; | |
614 | ||
615 | stream_node (stream, 0, root); | |
616 | ||
617 | return stream.str (); | |
618 | } | |
619 | ||
620 | // FIXME: Can we add selftests? | |
621 | ||
622 | } // namespace Resolver2_0 | |
623 | } // namespace Rust |