print_bindings/print_bindings.rs
1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8//! Walk a resolved AST and print every identifier with the binding it names.
9//!
10//! ```text
11//! cargo run -p hermes-sema --example print_bindings -- file.js
12//! ```
13//!
14//! With no argument it uses a built-in snippet that exercises several binding
15//! kinds. This is the canonical use of the two crates together — parse,
16//! resolve, walk, ask "what does this name mean?" — and it demonstrates the
17//! two things that are otherwise easy to get wrong:
18//!
19//! 1. turning an atom into a string, with the generated `name_str` accessor
20//! (see [`hermes_parser::ast::node::Identifier::name_str`]);
21//! 2. holding a `&GCLock` inside a [`Visitor`] without tripping over the
22//! lock's invariance — see the comment on `BindingPrinter` below.
23
24use hermes_parser::ast::context::GCLock;
25use hermes_parser::ast::node::Node;
26use hermes_parser::ast::visitor::Visitor;
27use hermes_parser::{parse_named, ParseFlags};
28use hermes_sema::sem_context::SemContext;
29use hermes_sema::{resolve_for_compile, CompileOptions};
30
31/// Prints one line per identifier: its name, whether it declares or uses a
32/// binding, and the binding's kind.
33///
34/// **The lifetimes are the point.** `GCLock<'ast, 'ctx>` holds a
35/// `&'ctx mut Context<'ast>`, and `&mut T` is invariant in `T`, so the lock is
36/// invariant in `'ast`: a `&GCLock<'static, '_>` cannot be coerced to a
37/// `&GCLock<'shorter, '_>`. `with_program` hands the closure exactly a
38/// `&GCLock<'static, '_>` together with nodes borrowed for a higher-ranked
39/// `'gc`, so the natural first attempt — reusing the visitor's `'gc` for the
40/// lock, i.e.
41///
42/// ```text
43/// struct BindingPrinter<'gc> { gc: &'gc GCLock<'gc, 'gc>, ... }
44/// impl<'gc> Visitor<'gc> for BindingPrinter<'gc> { ... }
45/// ```
46///
47/// — fails, because it demands `'ast == 'gc` and invariance refuses to shorten
48/// `'static` to get there:
49///
50/// ```text
51/// error: lifetime may not live long enough
52/// | let mut v = BindingPrinter { gc, sem };
53/// | ^^ this usage requires that
54/// | `'1` must outlive `'2`
55/// = note: the struct `GCLock<'ast, 'ctx>` is invariant over the
56/// parameter `'ast`
57/// ```
58///
59/// The working pattern is below: give the lock its **own** lifetime
60/// parameters and leave them unconstrained by the visitor's `'gc`, which the
61/// `impl` does by writing them as `'_`. `'gc` then does only its real job —
62/// tying `node` to the tree — and never has to equal the arena's `'ast`.
63struct BindingPrinter<'a, 'ast, 'ctx> {
64 /// The lock, for turning atoms into strings. Its `'ast`/`'ctx` are
65 /// deliberately independent of the `'gc` of the `Visitor` impl.
66 gc: &'a GCLock<'ast, 'ctx>,
67 /// The resolution results: which declaration each identifier names.
68 sem: &'a SemContext,
69 /// Rows collected during the walk; printing happens after the lock is
70 /// released, though printing inside the walk would work equally well.
71 rows: Vec<(String, &'static str, String)>,
72}
73
74// Note the `'_`s: the lock's lifetimes are *not* the visitor's `'gc`.
75impl<'gc> Visitor<'gc> for BindingPrinter<'_, '_, '_> {
76 fn visit_node(&mut self, node: &'gc Node<'gc>) {
77 if let Node::Identifier(id) = node {
78 // The atom → string path. `name` is a `Cell<NodeLabel>`, i.e. an
79 // index into the arena's atom table; `name_str` reads the cell and
80 // borrows the table's bytes as UTF-8. Use `gc.bytes(id.name.get())`
81 // instead when the exact (possibly WTF-8) bytes matter.
82 let name = id.name_str(self.gc).to_string();
83
84 let (role, binding) = if let Some(d) =
85 self.sem.get_declaration_decl(id)
86 {
87 ("decl", format!("{:?}", self.sem.decl(d).kind))
88 } else if id.unresolvable.get() {
89 // An enclosing `eval` or `with` can capture the name at run
90 // time, so the resolver refuses to commit to a declaration.
91 ("use", "(unresolvable: inside eval or with)".to_string())
92 } else if let Some(d) = self.sem.get_expression_decl(id) {
93 ("use", format!("{:?}", self.sem.decl(d).kind))
94 } else {
95 // Property keys, member accesses and label names are
96 // identifiers too, and none of them names a binding.
97 ("-", "(not a variable reference)".to_string())
98 };
99
100 self.rows.push((name, role, binding));
101 }
102 // The default `visit_node` does exactly this; recursion is ours to
103 // control, so a visitor can prune subtrees by not calling it.
104 node.visit_children(self);
105 }
106}
107
108const SOURCE: &str = r#"
109class Counter {
110 #n = 0;
111 step(by = 1) {
112 const before = this.#n;
113 this.#n += by;
114 return before;
115 }
116}
117
118let counter = new Counter();
119var total = 0;
120for (const step of [1, 2, 3]) {
121 total += counter.step(step);
122}
123console.log(total);
124"#;
125
126fn main() {
127 let (name, source) = match std::env::args().nth(1) {
128 Some(path) => match std::fs::read_to_string(&path) {
129 Ok(s) => (path, s),
130 Err(e) => {
131 eprintln!("print_bindings: cannot read '{path}': {e}");
132 std::process::exit(1);
133 }
134 },
135 None => ("<builtin>".to_string(), SOURCE.to_string()),
136 };
137
138 // Step 1: parse. `ParseFlags::default()` is plain ECMAScript.
139 let parsed = match parse_named(&source, &name, ParseFlags::default()) {
140 Ok(parsed) => parsed,
141 Err(e) => {
142 // `messages()` strings are already newline-terminated.
143 for m in e.messages() {
144 eprint!("{m}");
145 }
146 std::process::exit(2);
147 }
148 };
149
150 // Step 2: resolve. The compile path, so the standard globals exist and an
151 // undeclared `console` comes back as `UndeclaredGlobalProperty` rather
152 // than as nothing at all; `hermes_sema::resolve` is the parser path.
153 let mut resolved =
154 match resolve_for_compile(parsed, &CompileOptions::default()) {
155 Ok(resolved) => resolved,
156 Err(e) => {
157 for m in e.messages() {
158 eprint!("{m}");
159 }
160 std::process::exit(2);
161 }
162 };
163
164 // Step 3: walk. References into the arena die with the lock, so the
165 // visitor collects owned `String`s and hands them back out.
166 let rows = resolved.with_program(|gc, root, sem| {
167 let mut printer = BindingPrinter {
168 gc,
169 sem,
170 rows: Vec::new(),
171 };
172 printer.visit_node(root);
173 printer.rows
174 });
175
176 println!("{}: {} identifiers", name, rows.len());
177 for (name, role, binding) in rows {
178 println!(" {name:<12} {role:<5} {binding}");
179 }
180}