Skip to main content

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}