hax_rust_engine/printer/render_view.rs
1//! Tools for rendering Rust paths into strings.
2//!
3//! This module takes a typed [`View`] (a list of [`PathSegment`]s) and turns it
4//! into either:
5//! - a structured [`Rendered`] (with `module` vs. `path` parts), or
6//! - a single flat `String`.
7//!
8//! The [`RenderView`] trait allows for customization.
9
10use crate::{
11 ast::identifiers::global_id::{
12 ReservedSuffix,
13 view::{PathSegment, PathSegmentPayload, UnnamedPathSegmentPayload, View},
14 },
15 symbol::Symbol,
16};
17
18use std::collections::HashSet;
19use std::sync::OnceLock;
20
21/// A helper trait to render a [`View`] (a typed list of path segments) into
22/// strings.
23///
24/// Rendering is split into two parts:
25/// - module path: the crate + module prefix,
26/// - relative path: the remaining (non-module) segments, and both may contain
27/// hierarchical sub-segments (e.g. `Foo::MyVariant::field`).
28///
29/// Implementors can:
30/// - override how unnamed segments (e.g. `impl`, `anon const`) are displayed,
31/// - override how each segment is rendered,
32/// - customize the separator (defaults to `"::"`),
33/// - render to either a structured [`Rendered`] or a single flat `String`.
34///
35/// # Terminology
36///
37/// A path segment can be:
38/// - named: carries a `Symbol` that can be printed as-is,
39/// - unnamed: carries an [`UnnamedPathSegmentPayload`] (like `Impl`, `Closure`,
40/// …), which must be turned into a `Symbol` first (see
41/// [`RenderView::render_unnamed_path_segment_payload`]).
42///
43/// # Hierarchical segments
44///
45/// Some segments are actually small trees (e.g., field → constructor → type).
46/// [`RenderView::render_path_segment`] returns all display atoms for such a
47/// segment, so callers can flatten or join as needed.
48pub trait RenderView: Sized {
49 /// List of reserved keywords that will be escaped when rendering
50 fn reserved_keywords() -> &'static HashSet<String> {
51 static SET: OnceLock<HashSet<String>> = OnceLock::new();
52 SET.get_or_init(|| [].into_iter().collect())
53 }
54
55 /// Check if a string is a reserved keyword that needs escaping
56 fn is_reserved_keyword(id: &str) -> bool {
57 let reserved = Self::reserved_keywords();
58 reserved.contains(id)
59 }
60
61 /// Check if a string needs escaping
62 fn should_escape(id: &str) -> bool {
63 Self::is_reserved_keyword(id)
64 }
65
66 /// Escape a string if it needs escaping according to `Self::should_escape`
67 fn escape(id: &str) -> String {
68 // See https://github.com/cryspen/hax/issues/1866
69 let id = id.replace([' ', '<', '>'], "_");
70 if id.is_empty() {
71 "_ERROR_EMPTY_ID_".to_string()
72 } else if Self::should_escape(id.trim_start_matches("_")) {
73 format!("_{id}")
74 } else {
75 id
76 }
77 }
78
79 /// Converts an unnamed path segment payload into a printable [`Symbol`].
80 ///
81 /// Unnamed segments include `impl`, `anon const`, `inline const`, `foreign mod`,
82 /// `global_asm`, `use`, `opaque`, and `closure`. By default, these map to
83 /// their capitalized identifier (e.g., `Impl`, `AnonConst`, …).
84 ///
85 /// Override this method to customize how unnamed items appear in output.
86 fn render_unnamed_path_segment_payload(&self, unnamed: UnnamedPathSegmentPayload) -> Symbol {
87 default::render_unnamed_path_segment_payload(self, unnamed)
88 }
89
90 /// Converts a full [`PathSegmentPayload`] (named or unnamed) into a printable [`Symbol`].
91 ///
92 /// Named payloads return their `Symbol` unchanged. Unnamed payloads are delegated to
93 /// [`render_unnamed_path_segment_payload`].
94 fn render_path_segment_payload(&self, payload: PathSegmentPayload) -> Symbol {
95 match payload {
96 PathSegmentPayload::Named(symbol) => symbol,
97 PathSegmentPayload::Unnamed(unnamed) => {
98 self.render_unnamed_path_segment_payload(unnamed)
99 }
100 }
101 }
102
103 /// Renders a single [`PathSegment`] into a vector of display atoms.
104 ///
105 /// Most segments render to a single atom (e.g., `"Foo"`). Hierarchical segments
106 /// (like a field) render to multiple atoms representing their parent chain
107 /// (e.g., `["Foo", "MyVariant", "my_field"]`). Disambiguators (see
108 /// [`PathSegment::disambiguator`]) are suffixed as `_N` when `N > 0`.
109 ///
110 /// The resulting atoms are suitable for joining with [`separator`](Self::separator),
111 /// or for further grouping into module vs. relative path.
112 fn render_path_segment(&self, seg: &PathSegment) -> Vec<String> {
113 default::render_path_segment(self, seg)
114 }
115
116 /// Renders the optional suffix
117 fn render_suffix(&self, suffix: &ReservedSuffix) -> String {
118 default::render_suffix(suffix)
119 }
120
121 /// Renders just the module path (crate + modules) of a [`View`], as a list of atoms.
122 ///
123 /// This is a convenience wrapper around [`render`](Self::render) that returns only
124 /// the `module` component.
125 fn module(&self, view: &View) -> Vec<String> {
126 self.render(view).module
127 }
128
129 /// Allows backends to adjust a module path before rendering, e.g., to shorten it according
130 /// to currenly open namespaces.
131 fn relativize_module_path<'a>(&self, module_path: &'a [PathSegment]) -> &'a [PathSegment] {
132 module_path
133 }
134
135 /// Renders a [`View`] into a structured [`Rendered`] value,
136 /// splitting output into `module` and `path` parts.
137 ///
138 /// Internally, this uses [`View::split_at_module`] to separate module segments
139 /// from the remaining non-module segments, rendering each with
140 /// [`render_path_segment`].
141 fn render(&self, view: &View) -> Rendered {
142 let (module_path, relative_path) = view.split_at_module();
143 let module_path = self.relativize_module_path(module_path);
144 let path_segment = |seg| self.render_path_segment(seg);
145 let mut path: Vec<String> = relative_path.iter().flat_map(path_segment).collect();
146 if let Some(last) = path.last_mut()
147 && let Some(suffix) = view.suffix()
148 {
149 last.push_str(&self.render_suffix(suffix));
150 }
151 Rendered {
152 module: module_path.iter().flat_map(path_segment).collect(),
153 path,
154 }
155 }
156
157 /// Returns the string used to join rendered atoms (defaults to `"::"`).
158 ///
159 /// Override to customize separators (e.g., `"."`).
160 fn separator(&self) -> &str {
161 "::"
162 }
163
164 /// Lazy render a view as an iterator of strings.
165 ///
166 /// This chains `rendered.module` and `rendered.path` in order.
167 fn rendered_to_strings(&self, rendered: Rendered) -> impl Iterator<Item = String> {
168 rendered.module.into_iter().chain(rendered.path)
169 }
170
171 /// Joins the atoms contained in a [`Rendered`] into a single string using
172 /// [`separator`](Self::separator).
173 ///
174 /// This concatenates `rendered.module` and `rendered.path` in order, inserting
175 /// the separator between atoms.
176 fn rendered_to_string(&self, rendered: Rendered) -> String {
177 self.rendered_to_strings(rendered)
178 .collect::<Vec<_>>()
179 .join(self.separator())
180 }
181
182 /// Convenience: renders a [`View`] straight to a single `String`.
183 fn render_string(&self, view: &View) -> String {
184 self.rendered_to_string(self.render(view))
185 }
186
187 /// Convenience: renders a [`View`] straight to a iterator of `String`s.
188 fn render_strings(&self, view: &View) -> impl Iterator<Item = String> {
189 self.rendered_to_strings(self.render(view))
190 }
191}
192
193/// Default rendering helpers used by [`RenderView`]'s blanket implementations.
194///
195/// You can call these directly when composing your own renderer, or override the
196/// trait methods to change behavior selectively.
197pub mod default {
198 use super::*;
199
200 /// Default mapping of unnamed payloads to printable symbols.
201 pub fn render_unnamed_path_segment_payload<V: RenderView + Sized>(
202 _render_view: &V,
203 unnamed: UnnamedPathSegmentPayload,
204 ) -> Symbol {
205 Symbol::new(match unnamed {
206 UnnamedPathSegmentPayload::Impl => "Impl",
207 UnnamedPathSegmentPayload::AnonConst => "AnonConst",
208 UnnamedPathSegmentPayload::InlineConst => "InlineConst",
209 UnnamedPathSegmentPayload::Foreign => "Foreign",
210 UnnamedPathSegmentPayload::GlobalAsm => "GlobalAsm",
211 UnnamedPathSegmentPayload::Use => "Use",
212 UnnamedPathSegmentPayload::Opaque => "Opaque",
213 UnnamedPathSegmentPayload::Closure => "Closure",
214 })
215 }
216
217 /// Default rendering of a single [`PathSegment`] into display atoms.
218 ///
219 /// This walks the segment's parent chain (see [`PathSegment::parents`]) and
220 /// produces an atom for each level using
221 /// [`RenderView::render_path_segment_payload`]. If a level has a disambiguator
222 /// `> 0`, it is appended as `_<n>` (e.g., `Foo_2`).
223 pub fn render_path_segment<V: RenderView + Sized>(
224 render_view: &V,
225 seg: &PathSegment,
226 ) -> Vec<String> {
227 let mut strings: Vec<String> = seg
228 .parents()
229 .map(|seg| {
230 let id = render_view.render_path_segment_payload(seg.payload());
231 let d = seg.disambiguator();
232 if d > 0 {
233 format!("{id}_{d}")
234 } else {
235 format!("{id}")
236 }
237 })
238 .map(|str| V::escape(&str))
239 .collect();
240 strings.reverse();
241 strings
242 }
243
244 /// Default suffix rendering
245 pub fn render_suffix(suffix: &ReservedSuffix) -> String {
246 match suffix {
247 ReservedSuffix::Pre => "_pre",
248 ReservedSuffix::Post => "_post",
249 ReservedSuffix::Cast => "_cast_to_repr",
250 }
251 .to_owned()
252 }
253}
254
255/// The structured result of rendering a [`View`].
256///
257/// - `module`: atoms for the crate + modules prefix (may be empty for local/anonymous contexts),
258/// - `path`: atoms for the remaining segments (item, constructors, fields, etc.).
259///
260/// Join with [`RenderView::rendered_to_string`] to obtain a single string.
261pub struct Rendered {
262 /// Crate + module atoms (e.g., `["my_crate", "a", "b"]`).
263 pub module: Vec<String>,
264 /// Non-module atoms (e.g., `["Foo::f", "MyEnum::MyVariant::my_field"]`).
265 pub path: Vec<String>,
266}