Skip to main content

hermes_parser/
lib.rs

1//! A Rust port of the Hermes JavaScript front end — lexer and parser.
2//!
3//! Faithful 1:1 port of the C++ `JSLexer` and `JSParserImpl`, validated
4//! byte-for-byte against `hermesc -dump-ast` over a per-dialect corpus. The
5//! output is the ESTree AST of the `ast` crate. Every dialect the C++ parser
6//! supports is complete and covered by that differential gate: ECMAScript,
7//! the Flow type grammar, TypeScript, and JSX. The three non-standard ones
8//! are opt-in through the same `hermes_ast::context::Context` flags as in the C++
9//! (`parse_flow` and its four extension flags, `parse_ts`, `parse_jsx`).
10//!
11//! # Quickstart
12//!
13//! ```
14//! use hermes_parser::ast::node::Node;
15//! use hermes_parser::{parse, ParseFlags};
16//!
17//! let flags = ParseFlags::default();
18//! let mut parsed = parse("1 + 2;", flags).expect("parse error");
19//!
20//! // The AST lives in an arena owned by `parsed`; read it under a lock.
21//! let statements = parsed.with_program(|_gc, program| match program {
22//!     Node::Program(p) => p.body.iter().count(),
23//!     _ => unreachable!("the root of a parse is always a Program"),
24//! });
25//! assert_eq!(statements, 1);
26//!
27//! // Or dump it the way `hermesc -dump-ast` does.
28//! let json = parsed.to_estree_json(false);
29//! assert!(json.starts_with(r#"{"type":"Program""#));
30//! ```
31//!
32//! ## Names and string values: from atom to `&str`
33//!
34//! Text in the AST is *interned*: `id.name` is a `Cell<NodeLabel>`, an index
35//! into the arena's atom table, not a `String`. Every text field has a
36//! generated accessor that does the lookup against the [`ast::context::GCLock`],
37//! returning a `&str` that borrows the table's bytes — no allocation:
38//!
39//! ```
40//! use hermes_parser::ast::node::Node;
41//! use hermes_parser::{parse, ParseFlags};
42//!
43//! let src = r#"function greet(who) { return "hi"; }"#;
44//! let mut parsed = parse(src, ParseFlags::default()).expect("parse error");
45//!
46//! let names = parsed.with_program(|gc, program| {
47//!     let Node::Program(p) = program else { unreachable!() };
48//!     let Some(Node::FunctionDeclaration(f)) = p.body.iter().next()
49//!     else { unreachable!() };
50//!     let Some(Node::Identifier(id)) = f.id else { unreachable!() };
51//!     let Some(Node::Identifier(param)) = f.params.iter().next()
52//!     else { unreachable!() };
53//!     // `<field>_str` for a name-like field…
54//!     (id.name_str(gc).to_string(), param.name_str(gc).to_string())
55//! });
56//! assert_eq!(names, ("greet".to_string(), "who".to_string()));
57//!
58//! let mut parsed = parse(r#""hi";"#, ParseFlags::default()).expect("parse");
59//! let value = parsed.with_program(|gc, program| {
60//!     let Node::Program(p) = program else { unreachable!() };
61//!     let Some(Node::ExpressionStatement(st)) = p.body.iter().next()
62//!     else { unreachable!() };
63//!     let Node::StringLiteral(s) = st.expression else { unreachable!() };
64//!     // …but `try_<field>_str` for a string *value*, which can legally be an
65//!     // unpaired surrogate and then has no UTF-8 form at all.
66//!     s.try_value_str(gc).map(str::to_string)
67//! });
68//! assert_eq!(value.as_deref(), Some("hi"));
69//! ```
70//!
71//! The split is deliberate. Name-like fields (identifiers, operators,
72//! keyword-like kinds) get a plain `<field>_str` that substitutes U+FFFD in
73//! the case that should not arise — the lexer rejects an identifier containing
74//! an unpaired surrogate. String-literal values get `try_<field>_str`
75//! returning `Option<&str>` and an explicit `<field>_str_lossy`, because a
76//! lone surrogate there is a legal JS value, and silently replacing it would
77//! corrupt the program a codegen tool round-trips. An astral character such as
78//! `"😀"` is *not* the `None` case: it is stored as a WTF-8 surrogate pair and
79//! both accessors fold it back into the character. For the exact stored bytes,
80//! use [`ast::context::GCLock::bytes`]; [`ast::context::GCLock::bytes_str_lossy`]
81//! and [`ast::context::GCLock::try_bytes_str`] are the same conversions for an
82//! atom you already hold.
83//!
84//! `crates/sema/examples/print_bindings.rs` puts this together with a
85//! `Visitor` walk and name resolution.
86//!
87//! The pieces a consumer touches:
88//! - [`parse`] / [`parse_named`] returning [`ParsedJS`] — the convenience
89//!   façade, which assembles an [`hermes_ast::context::Context`], a
90//!   `SourceErrorManager`, a [`lexer::JSLexer`] and a [`js::JSParserImpl`]
91//!   into one call. It adds no behavior; anything it does not expose is
92//!   reachable by driving those pieces directly.
93//! - [`ast`] — the AST crate, re-exported so that depending on this crate
94//!   alone is enough to name [`hermes_ast::node::Node`], walk with
95//!   [`hermes_ast::visitor::Visitor`], or drive [`hermes_ast::dump`] by hand.
96//! - [`js::JSParserImpl`] — the recursive-descent parser; `new` + `parse`
97//!   returns the `Program` node, or `None` after a reported error.
98//! - [`lexer::JSLexer`] — the lexer, usable on its own; it reports through a
99//!   `hermes_support::manager::SourceErrorManager` and interns into an `AtomTable`.
100//! - [`token::Token`] and [`token_kinds::TokenKind`] — the token surface, the
101//!   latter generated from `include/hermes/Parser/TokenKinds.def` order.
102//! - [`js::ParserPass`] — `FullParse` (eager), plus the `PreParse`/`LazyParse`
103//!   pair that indexes function bodies in one scan and defers parsing them.
104//! - [`json`] — the separate `JSONParser` port (a distinct grammar sharing the
105//!   same lexer), with the uniquing/hidden-class `JSONFactory`.
106//!
107//! The remaining modules are the lexer's own building blocks: [`cursor`] (the
108//! scan cursor), [`number`] (numeric-literal conversion), [`utf8`] (the
109//! UTF-8/UTF-16 conversions the C++ keeps in `Support`), and
110//! [`html_entities`] (the JSX entity table generated from `HTMLEntities.def`).
111//! Only the port's internals call them, and no public signature in this crate
112//! mentions them; they are public incidentally rather than by design, and may
113//! be demoted to `pub(crate)` in a future release.
114//!
115//! See `rust/ARCHITECTURE.md` for the design rationale and
116//! doc/superpowers/specs/2026-06-06-js-parser-design.md for the port spec.
117
118#![warn(missing_docs)]
119
120pub mod cursor;
121pub mod html_entities;
122pub mod js;
123pub mod json;
124pub mod lexer;
125pub mod number;
126pub mod token;
127pub mod token_kinds;
128pub mod utf8;
129
130/// The façade module is private: its items are re-exported here so each has
131/// exactly one path in the docs.
132mod facade;
133
134pub use facade::{parse, parse_named, ParseError, ParseFlags, ParsedJS};
135
136/// The AST crate, re-exported under the short name `ast`, so the public path
137/// is `hermes_parser::ast`. Parsing hands back AST types, so a consumer needs
138/// them; re-exporting keeps this crate the only dependency they must declare.
139/// `ast::node::Node`, `ast::visitor`, `ast::context::GCLock` and `ast::dump`
140/// are the pieces the façade's signatures mention. The same items are also
141/// reachable as `hermes_ast::…` by depending on that crate directly.
142pub use hermes_ast as ast;
143
144/// One recorded diagnostic, re-exported because it appears in the façade's
145/// signatures ([`ParseError::diagnostics`], [`ParsedJS::diagnostics`]).
146/// Render one with `hermes_support::render::render_diagnostic`.
147pub use hermes_support::diag::ResolvedDiagnostic;