Skip to main content

hermes_parser/js/
pre_lazy.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//! The Pre/Lazy parser passes. Port of the `ParserPass` machinery in
9//! `lib/Parser/JSParserImpl.{h,cpp}` and `include/hermes/Parser/JSParser.h`.
10
11use std::cell::Cell;
12use std::collections::HashMap;
13use std::rc::Rc;
14
15use hermes_support::location::SMLoc;
16
17/// The parser mode. Port of `enum ParserPass` (JSParser.h:26-36). Same order:
18/// PreParse, LazyParse, FullParse.
19#[derive(Clone, Copy, PartialEq, Eq, Debug)]
20pub enum ParserPass {
21    /// Parse and index the file's functions without keeping an AST.
22    PreParse,
23    /// Re-parse, skipping function bodies indexed by a prior PreParse.
24    LazyParse,
25    /// Completely parse the full file (the default, eager mode).
26    FullParse,
27}
28
29/// Information about a pre-parsed function body, recorded during the
30/// `PreParse` pass and consumed during `LazyParse`.
31/// Port of `PreParsedFunctionInfo` (PreParser.h:38-58).
32#[derive(Clone)]
33pub struct PreParsedFunctionInfo {
34    /// The end location of the function body (closing `}`).
35    pub end: SMLoc,
36
37    /// Whether the function body began with `"use strict"`.
38    pub strict_mode: bool,
39
40    /// Directive prologues found at the top of the function body.
41    /// Stored as owned byte vectors because UniqueString atoms are
42    /// arena-allocated and reclaimed between parse passes — we cannot hold
43    /// raw pointers across pass boundaries (PreParser.h:46-48).
44    pub directives: Vec<Vec<u8>>,
45
46    /// Whether the function body contains an arrow function.
47    pub contains_arrow_functions: bool,
48
49    /// Conservative estimate: whether a non-arrow function may have an arrow
50    /// child that references `arguments`, requiring eager Arguments capture.
51    pub may_contain_arrow_functions_using_arguments: bool,
52}
53
54/// Per-buffer table produced by the `PreParse` pass.
55/// Port of `PreParsedBufferInfo` (PreParser.h:60-63).
56#[derive(Clone)]
57pub struct PreParsedBufferInfo {
58    /// Maps function-body start **offset** (within the source buffer) to its
59    /// pre-parsed metadata. The C++ uses `SMLoc` (a pointer) as the key;
60    /// we use the `u32` offset so the map is trivially `Send`/serialisable.
61    pub function_info: HashMap<u32, PreParsedFunctionInfo>,
62}
63
64/// RAII Drop-guard for the three arrow-bookkeeping flags
65/// (`isArrowFunction_`, `containsArrowFunctions_`,
66/// `mayContainArrowFunctionsUsingArguments_`). Owns `Rc<Cell<bool>>` clones of
67/// each flag so it can restore them on Drop without borrowing `self` — the same
68/// pattern as `ParamFlagGuard` in mod.rs. Strict-mode and `seen_directives`
69/// are managed separately at each call site (they live on `&mut self` fields
70/// that cannot be owned by the guard).
71///
72/// Port of `JSParserImpl::SaveFunctionState` (JSParserImpl.h:1699-1740).
73pub(super) struct SaveFunctionState {
74    is_arrow: Rc<Cell<bool>>,
75    contains: Rc<Cell<bool>>,
76    may_contain: Rc<Cell<bool>>,
77    old_is_arrow: bool,
78    old_contains: bool,
79    old_may_contain: bool,
80}
81
82impl Drop for SaveFunctionState {
83    fn drop(&mut self) {
84        // C++ dtor JSParserImpl.h:1728-1738.
85        if !self.is_arrow.get() {
86            self.contains.set(self.old_contains);
87            self.may_contain.set(self.old_may_contain);
88        }
89        self.is_arrow.set(self.old_is_arrow);
90    }
91}
92
93use hermes_ast::node::{Node, NodeKind};
94
95use crate::lexer::{GrammarContext, JSLexer};
96
97use super::flow::{AllowTypedArrowFunction, CoverTypedParameters};
98use super::{JSParserImpl, PARAM_IN, PARAM_RETURN};
99
100impl<'gc, 'ast, 'ctx, 'a> JSParserImpl<'gc, 'ast, 'ctx, 'a> {
101    /// Port of `JSParserImpl::preParseBuffer` (JSParserImpl.cpp:7534-7546).
102    /// The C++ `PreParser` wrapper holds an `AllocationScope` (cpp:7523)
103    /// that reclaims the whole pass AST when the returned shared_ptr dies;
104    /// here the scope is opened around `parse()` and dropped before
105    /// returning — tighter, and sound because the `Program` result is
106    /// discarded and `JSParserImpl` holds no node references. The pass
107    /// output is the side-table + parser flags only.
108    pub fn pre_parse_buffer(
109        gc: &'gc hermes_ast::context::GCLock<'ast, 'ctx>,
110        lexer: JSLexer<'a>,
111        strict: bool,
112    ) -> Option<JSParserImpl<'gc, 'ast, 'ctx, 'a>> {
113        let mut p = JSParserImpl::new_with_pass(gc, lexer, ParserPass::PreParse);
114        p.lexer.set_strict_mode(strict);
115        // SAFETY: the only node reference produced inside the scope is the
116        // `Program` result, consumed by `.is_some()` before the drop.
117        #[allow(unsafe_code)] // alloc_scope mirrors C++ AllocationScope
118        let scope = unsafe { gc.alloc_scope() };
119        let ok = p.parse().is_some();
120        drop(scope);
121        if !ok {
122            return None;
123        }
124        Some(p)
125    }
126
127    /// Construct a `SaveFunctionState` guard that saves and restores the three
128    /// arrow-bookkeeping flags on Drop. Also sets the flags for the new
129    /// function scope. Port of the `SaveFunctionState` ctor
130    /// (JSParserImpl.h:1719-1726).
131    pub(super) fn save_function_state(&self, is_arrow: bool) -> SaveFunctionState {
132        let g = SaveFunctionState {
133            is_arrow: Rc::clone(&self.is_arrow_function),
134            contains: Rc::clone(&self.contains_arrow_functions),
135            may_contain: Rc::clone(
136                &self.may_contain_arrow_functions_using_arguments,
137            ),
138            old_is_arrow: self.is_arrow_function.get(),
139            old_contains: self.contains_arrow_functions.get(),
140            old_may_contain: self
141                .may_contain_arrow_functions_using_arguments
142                .get(),
143        };
144        // C++ ctor JSParserImpl.h:1719-1726.
145        self.is_arrow_function.set(is_arrow);
146        if is_arrow {
147            self.contains_arrow_functions.set(true);
148        } else {
149            self.contains_arrow_functions.set(false);
150            self.may_contain_arrow_functions_using_arguments.set(false);
151        }
152        g
153    }
154
155    /// Return a copy of the directive list for the current function scope.
156    /// Port of `copySeenDirectives` (JSParserImpl.cpp:731-739).
157    #[allow(dead_code)]
158    pub(super) fn copy_seen_directives(&self) -> Vec<Vec<u8>> {
159        self.seen_directives.clone()
160    }
161
162    /// Move the parser to `loc` and re-lex the current token from there.
163    /// Port of `JSParserImpl::seek` (JSParserImpl.h:128-131): the C++ does
164    /// `lexer_.seek(startPos); tok_ = lexer_.advance();`. Our lexer keeps the
165    /// current token internally, so we seek the lexer cursor then `advance`
166    /// (with `AllowRegExp`, matching the parameterless C++ `lexer_.advance()`).
167    pub(super) fn seek(&mut self, loc: SMLoc) {
168        self.lexer.seek(loc);
169        self.advance(GrammarContext::AllowRegExp);
170    }
171
172    /// On-demand parse of a single deferred function body. Called when a
173    /// previously lazy-stubbed function is first executed: the parser is seeked
174    /// back to `start` and the function is re-parsed eagerly so its real body
175    /// (instead of the lazy stub) is produced.
176    ///
177    /// Port of `JSParserImpl::parseLazyFunction` (JSParserImpl.cpp:7548-7600).
178    /// `kind` selects which eager entry point to drive; `param_yield`/
179    /// `param_await` restore the grammar context the function was originally
180    /// parsed in. Returns the re-parsed function node (the `FunctionExpression`,
181    /// `FunctionDeclaration`, or `ArrowFunctionExpression`), or — for accessors
182    /// and class methods — the `value` function extracted from the wrapping
183    /// `Property`/`MethodDefinition` node (cpp:7572,7591).
184    pub fn parse_lazy_function(
185        &mut self,
186        kind: NodeKind,
187        param_yield: bool,
188        param_await: bool,
189        start: SMLoc,
190    ) -> Option<&'gc Node<'gc>> {
191        // cpp:7553-7556. Seek to the deferred function's start and restore
192        // the grammar context it was originally parsed in.
193        // Strict mode is the CALLER's responsibility (mirroring HBC.cpp:158:
194        // `parser.setStrictMode(lazyData.strictMode)` before
195        // `parseLazyFunction`); this function does not touch it.
196        self.seek(start);
197        self.param_yield.set(param_yield);
198        self.param_await.set(param_await);
199
200        match kind {
201            // cpp:7559-7560.
202            NodeKind::FunctionExpression => {
203                self.parse_function_expression(/* force_eagerly= */ true)
204            }
205
206            // cpp:7562-7563.
207            NodeKind::FunctionDeclaration => {
208                self.parse_function_declaration(
209                    PARAM_RETURN,
210                    /* force_eagerly= */ true,
211                )
212            }
213
214            // cpp:7565-7566. parseAssignmentExpression(ParamIn, /*eagerly*/true)
215            // with the header defaults for the remaining args.
216            NodeKind::ArrowFunctionExpression => self.parse_assignment_expression(
217                PARAM_IN,
218                /* force_eagerly= */ true,
219                AllowTypedArrowFunction::Yes,
220                CoverTypedParameters::Yes,
221                None,
222            ),
223
224            // cpp:7568-7579. Re-parse the property; the deferred function is its
225            // `value`. `dyn_cast<PropertyNode>` failure is not technically
226            // unreachable (a fudged source buffer), so we just return None.
227            NodeKind::Property => {
228                let node = self.parse_property_assignment(/* eagerly= */ true)?;
229                match node {
230                    Node::Property(prop) => Some(prop.value),
231                    _ => {
232                        debug_assert!(
233                            false,
234                            "Expected a getter/setter function"
235                        );
236                        None
237                    }
238                }
239            }
240
241            // cpp:7581-7595. Re-parse a single class element; the deferred
242            // function is the `value` of the resulting `MethodDefinition`.
243            // Strict mode must already be set by the caller (class bodies are
244            // always strict) so that `static` is lexed as `rw_static`.
245            NodeKind::MethodDefinition => {
246                let mut body: Vec<&'gc Node<'gc>> = Vec::new();
247                let mut constructor: Option<&'gc Node<'gc>> = None;
248                let success = self.parse_class_body_impl(
249                    &mut body,
250                    &mut constructor,
251                    /* eagerly= */ true,
252                );
253                if !success || body.len() != 1 {
254                    debug_assert!(false, "Unexpected parse_class_body_impl result");
255                    None
256                } else {
257                    match body[0] {
258                        Node::MethodDefinition(method) => Some(method.value),
259                        _ => {
260                            debug_assert!(false, "Expected MethodDefinitionNode");
261                            None
262                        }
263                    }
264                }
265            }
266
267            // cpp:7597-7598.
268            _ => unreachable!("Asked to parse unexpected node type"),
269        }
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    // A parser built with `new` defaults to FullParse; new_with_pass honors the arg.
276    #[test]
277    fn parser_pass_defaults_and_override() {
278        use hermes_ast::context::Context;
279        use hermes_support::manager::SourceErrorManager;
280        use crate::lexer::{GrammarContext, JSLexer};
281        use crate::js::{JSParserImpl, ParserPass};
282
283        let mut sm = SourceErrorManager::new();
284        let id = sm.add_buffer_bytes("t", b"1;");
285        let mut ctx = Context::new();
286        let gc = ctx.lock();
287        let atoms = &gc.ctx().atom_table;
288        let lexer = JSLexer::new(id, &mut sm, atoms, GrammarContext::AllowRegExp);
289        let p = JSParserImpl::new_with_pass(&gc, lexer, ParserPass::PreParse);
290        assert_eq!(p.pass, ParserPass::PreParse);
291    }
292
293    // The side-table round-trips through take/set; threshold defaults to 0.
294    #[test]
295    fn pre_parsed_table_and_threshold() {
296        use hermes_ast::context::Context;
297        let mut ctx = Context::new();
298        assert_eq!(ctx.preemptive_function_compilation_threshold(), 0);
299        ctx.set_preemptive_function_compilation_threshold(64);
300        assert_eq!(ctx.preemptive_function_compilation_threshold(), 64);
301    }
302
303    // SaveFunctionState restores the three arrow-bookkeeping flags on Drop.
304    // Strict-mode is managed separately (lexer field, not Rc<Cell>), so it is
305    // not asserted here — that restore is done explicitly by each call-site.
306    #[test]
307    fn save_function_state_restores_on_drop() {
308        use hermes_ast::context::Context;
309        use hermes_support::manager::SourceErrorManager;
310        use crate::lexer::{GrammarContext, JSLexer};
311        use crate::js::{JSParserImpl, ParserPass};
312
313        let mut sm = SourceErrorManager::new();
314        let id = sm.add_buffer_bytes("t", b"0");
315        let mut ctx = Context::new();
316        let gc = ctx.lock();
317        let atoms = &gc.ctx().atom_table;
318        let lexer = JSLexer::new(id, &mut sm, atoms, GrammarContext::AllowRegExp);
319        let mut p = JSParserImpl::new_with_pass(&gc, lexer, ParserPass::PreParse);
320
321        p.lexer.set_strict_mode(false);
322        p.contains_arrow_functions.set(false);
323        {
324            // Enter a NON-arrow function: flags reset to false, restored on drop.
325            let _g = p.save_function_state(false);
326            // Strict mode is managed separately by callers (not by the guard),
327            // so we don't set it here — the guard doesn't own the lexer.
328            p.contains_arrow_functions.set(true);
329        }
330        // contains_arrow_functions was true inside the scope but the Drop
331        // impl restores old_contains (false) because is_arrow is false.
332        assert!(!p.contains_arrow_functions.get(), "contains_arrow restored");
333        // Verify strict-mode restore is the caller's responsibility.
334        assert!(!p.lexer.is_strict_mode(), "strict was never changed by guard");
335    }
336
337    // PreParse over a file with two functions records both, with correct strict
338    // flag and directives.
339    #[test]
340    fn preparse_records_functions() {
341        use hermes_ast::context::Context;
342        use hermes_support::manager::SourceErrorManager;
343        use crate::lexer::{GrammarContext, JSLexer};
344        use crate::js::{JSParserImpl, ParserPass};
345
346        let src = b"function a(){ 'use strict'; return 1; }\nvar b = () => 2;\n";
347        let mut sm = SourceErrorManager::new();
348        let id = sm.add_buffer_bytes("t", src);
349        let mut ctx = Context::new();
350        let gc = ctx.lock();
351        let atoms = &gc.ctx().atom_table;
352        let lexer = JSLexer::new(id, &mut sm, atoms, GrammarContext::AllowRegExp);
353        let mut p = JSParserImpl::new_with_pass(&gc, lexer, ParserPass::PreParse);
354        assert!(p.parse().is_some());
355        let t = p.take_pre_parsed();
356        // function a's body { ... } and the arrow are both recorded.
357        assert_eq!(t.function_info.len(), 2);
358        // exactly one recorded function is strict (function a, due to 'use strict').
359        let strict_count =
360            t.function_info.values().filter(|i| i.strict_mode).count();
361        assert_eq!(strict_count, 1);
362        let with_dir = t
363            .function_info
364            .values()
365            .filter(|i| !i.directives.is_empty())
366            .count();
367        assert_eq!(with_dir, 1);
368    }
369
370    /// Walk the AST looking for a BlockStatement with
371    /// `is_lazy_function_body == true`.
372    fn has_lazy_stub<'gc>(node: &'gc hermes_ast::node::Node<'gc>) -> bool {
373        use hermes_ast::node::Node;
374        use hermes_ast::visitor::Visitor;
375
376        struct LazyFinder(bool);
377        impl<'gc> Visitor<'gc> for LazyFinder {
378            fn visit_node(&mut self, node: &'gc Node<'gc>) {
379                if self.0 {
380                    return;
381                }
382                if let Node::BlockStatement(b) = node {
383                    if b.is_lazy_function_body.get() {
384                        self.0 = true;
385                        return;
386                    }
387                }
388                node.visit_children(self);
389            }
390        }
391
392        let mut finder = LazyFinder(false);
393        finder.visit_node(node);
394        finder.0
395    }
396
397    /// Find the first `FunctionDeclaration` node in the AST and return it.
398    fn find_function_decl<'gc>(
399        node: &'gc hermes_ast::node::Node<'gc>,
400    ) -> Option<&'gc hermes_ast::node::Node<'gc>> {
401        use hermes_ast::node::Node;
402        use hermes_ast::visitor::Visitor;
403
404        struct FnFinder<'gc>(Option<&'gc Node<'gc>>);
405        impl<'gc> Visitor<'gc> for FnFinder<'gc> {
406            fn visit_node(&mut self, node: &'gc Node<'gc>) {
407                if self.0.is_some() {
408                    return;
409                }
410                if let Node::FunctionDeclaration(_) = node {
411                    self.0 = Some(node);
412                    return;
413                }
414                node.visit_children(self);
415            }
416        }
417
418        let mut finder = FnFinder(None);
419        finder.visit_node(node);
420        finder.0
421    }
422
423    // Demand-parsing a deferred function reproduces a non-stub body. We first
424    // PreParse + LazyParse (threshold 0) to get a skeleton whose function body
425    // is a lazy stub, then call `parse_lazy_function` at the function's start
426    // and assert the re-parsed body is eager (not a stub) and non-empty.
427    #[test]
428    fn parse_lazy_function_reparses_body() {
429        use hermes_ast::context::Context;
430        use hermes_ast::node::{Node, NodeKind};
431        use hermes_support::manager::SourceErrorManager;
432        use crate::lexer::{GrammarContext, JSLexer};
433        use crate::js::{JSParserImpl, ParserPass};
434
435        let src = b"function a(){ return 1 + 2; }\n";
436        let mut sm = SourceErrorManager::new();
437        let id = sm.add_buffer_bytes("t", src);
438        let mut ctx = Context::new();
439        ctx.set_preemptive_function_compilation_threshold(0); // defer everything
440        let gc = ctx.lock();
441        let atoms = &gc.ctx().atom_table;
442        // First PreParse to build the table.
443        let table = {
444            let l = JSLexer::new(id, &mut sm, atoms, GrammarContext::AllowRegExp);
445            let mut pp =
446                JSParserImpl::new_with_pass(&gc, l, ParserPass::PreParse);
447            pp.parse().unwrap();
448            pp.take_pre_parsed()
449        };
450        // LazyParse to build the skeleton with a lazy-stub body.
451        let l = JSLexer::new(id, &mut sm, atoms, GrammarContext::AllowRegExp);
452        let mut lp =
453            JSParserImpl::new_with_pass(&gc, l, ParserPass::LazyParse);
454        lp.set_pre_parsed(table);
455        let prog = lp.parse().unwrap();
456        assert!(has_lazy_stub(prog), "skeleton body should be a lazy stub");
457
458        // Grab the FunctionDeclaration's start location from the skeleton.
459        let func = find_function_decl(prog).expect("FunctionDeclaration");
460        let start = func.range().start;
461
462        // Demand-parse the deferred function body eagerly.
463        let body = lp
464            .parse_lazy_function(NodeKind::FunctionDeclaration, false, false, start)
465            .expect("parse_lazy_function should succeed");
466
467        // The result is a FunctionDeclaration whose body is a real (non-stub)
468        // BlockStatement containing the `return 1 + 2;` statement.
469        let Node::FunctionDeclaration(fd) = body else {
470            panic!("expected a FunctionDeclaration node");
471        };
472        let Node::BlockStatement(block) = fd.body else {
473            panic!("expected a BlockStatement body");
474        };
475        assert!(
476            !block.is_lazy_function_body.get(),
477            "re-parsed body must NOT be a lazy stub"
478        );
479        assert!(
480            !block.body.is_empty(),
481            "re-parsed body must contain statements"
482        );
483        // The single statement is the `return 1 + 2;`.
484        assert!(!has_lazy_stub(body), "re-parsed function has no lazy stub");
485    }
486
487    // Site 1 (cpp:516-560): PreParse reclaims each function body when the
488    // function completes. Measured by driving a PreParse parser manually
489    // (no whole-pass scope yet) and comparing node counts against an eager
490    // parse of the same source: the retained PreParse AST is the skeleton
491    // spine + blank-bodied keepers, a small fraction of the full AST.
492    #[test]
493    fn preparse_reclaims_function_bodies() {
494        use hermes_ast::context::Context;
495        use hermes_support::manager::SourceErrorManager;
496        use crate::lexer::{GrammarContext, JSLexer};
497        use crate::js::{JSParserImpl, ParserPass};
498
499        // One function with a fat body (~20 statements), repeated 50x.
500        let mut src: Vec<u8> = Vec::new();
501        for f in 0..50 {
502            src.extend_from_slice(format!("function f{f}(a, b) {{\n").as_bytes());
503            for i in 0..20 {
504                src.extend_from_slice(
505                    format!("  var x{i} = a + b * {i};\n").as_bytes(),
506                );
507            }
508            src.extend_from_slice(b"  return a;\n}\n");
509        }
510
511        let count_nodes = |pass: ParserPass| -> usize {
512            let mut sm = SourceErrorManager::new();
513            let id = sm.add_buffer_bytes("t", &src);
514            let mut ctx = Context::new();
515            let gc = ctx.lock();
516            let atoms = &gc.ctx().atom_table;
517            let lexer =
518                JSLexer::new(id, &mut sm, atoms, GrammarContext::AllowRegExp);
519            let mut p = JSParserImpl::new_with_pass(&gc, lexer, pass);
520            assert!(p.parse().is_some(), "parse failed");
521            gc.ctx().num_nodes()
522        };
523
524        let eager = count_nodes(ParserPass::FullParse);
525        let pre = count_nodes(ParserPass::PreParse);
526        // Shape assertion, generous constant: the keepers + spine must be a
527        // small fraction of the full AST (each body is ~20x its keeper).
528        assert!(
529            pre * 5 < eager,
530            "PreParse retained O(file) AST: pre={pre} eager={eager}"
531        );
532    }
533
534    // LazyParse with threshold 0 defers a function body: the BlockStatement
535    // is a lazy stub.
536    #[test]
537    fn lazyparse_defers_body() {
538        use hermes_ast::context::Context;
539        use hermes_support::manager::SourceErrorManager;
540        use crate::lexer::{GrammarContext, JSLexer};
541        use crate::js::{JSParserImpl, ParserPass};
542
543        let src = b"function a(){ return 1 + 2; }\n";
544        let mut sm = SourceErrorManager::new();
545        let id = sm.add_buffer_bytes("t", src);
546        let mut ctx = Context::new();
547        ctx.set_preemptive_function_compilation_threshold(0); // defer everything
548        let gc = ctx.lock();
549        let atoms = &gc.ctx().atom_table;
550        // First PreParse to build the table.
551        let table = {
552            let l = JSLexer::new(id, &mut sm, atoms, GrammarContext::AllowRegExp);
553            let mut pp =
554                JSParserImpl::new_with_pass(&gc, l, ParserPass::PreParse);
555            pp.parse().unwrap();
556            pp.take_pre_parsed()
557        };
558        let l = JSLexer::new(id, &mut sm, atoms, GrammarContext::AllowRegExp);
559        let mut lp =
560            JSParserImpl::new_with_pass(&gc, l, ParserPass::LazyParse);
561        lp.set_pre_parsed(table);
562        let prog = lp.parse().unwrap();
563        // Walk to the function's body and assert it's a lazy stub.
564        assert!(
565            has_lazy_stub(prog),
566            "expected a lazy function body stub"
567        );
568    }
569}
570