facebook/flow · error

Popping lex mode from empty stack

Error message

Popping lex mode from empty stack

What it means

The lexer mode stack tracks nested lexical contexts: push_lex_mode pushes a LexMode and resets the lookahead; pop_lex_mode (rust_port/crates/flow_parser/src/parser_env.rs:2549) pops and expects the stack to be non-empty. This panic is a stack underflow: the parser popped more lex modes than it pushed. Because double_pop_lex_mode pops twice, the same message can originate from three adjacent lines (2551, 2559, 2562).

Source

Thrown at rust_port/crates/flow_parser/src/parser_env.rs:2562

    /* [maybe env t] eats the next token and returns [true] if it is [t], else return [false] */
    pub(crate) fn maybe(env: &mut ParserEnv, t: TokenKind) -> Result<bool, Rollback> {
        let is_t = peek::token(env) == &t;
        if is_t {
            token(env)?;
        }
        Ok(is_t)
    }

    pub(crate) fn push_lex_mode(env: &mut ParserEnv, mode: LexMode) {
        env.lex_mode_stack.push(mode);
        let new_lex_mode = env.lex_mode();
        env.lookahead.reset(new_lex_mode);
    }

    pub(crate) fn pop_lex_mode(env: &mut ParserEnv) {
        env.lex_mode_stack
            .pop()
            .expect("Popping lex mode from empty stack");
        let new_lex_mode = env.lex_mode();
        env.lookahead.reset(new_lex_mode);
    }

    pub(crate) fn double_pop_lex_mode(env: &mut ParserEnv) {
        env.lex_mode_stack
            .pop()
            .expect("Popping lex mode from empty stack");
        env.lex_mode_stack
            .pop()
            .expect("Popping lex mode from empty stack");
        let new_lex_mode = env.lex_mode();
        env.lookahead.reset(new_lex_mode);
    }

    pub(crate) fn rescan_as_template_from<'a>(
        env: &mut ParserEnv<'a>,
        prev_cursor: wrapped_lex_env::WrappedLexCursor,

View on GitHub (pinned to 5c86586199)

Solutions

  1. Capture the exact panicking input and stack trace; identify the parse rule owning the pop_lex_mode call site
  2. Audit that rule for a push_lex_mode without a matching pop on every path, especially error/rollback paths
  3. Compare against upstream Flow parser behavior on the same input to see which side is unbalanced
  4. Upgrade flow_parser; lexer-mode balancing regressions are usually fixed quickly after fuzz reports
  5. If you maintain the code, use a checked pop that records an internal parse error instead of panicking

Example fix

// before: blind pop panics on underflow
pub(crate) fn pop_lex_mode(env: &mut ParserEnv) {
    env.lex_mode_stack.pop().expect("Popping lex mode from empty stack");
    let new_lex_mode = env.lex_mode();
    env.lookahead.reset(new_lex_mode);
}

// after: checked pop surfaces an internal error instead of crashing
pub(crate) fn pop_lex_mode(env: &mut ParserEnv) {
    if env.lex_mode_stack.pop().is_none() {
        env.error_at_current_loc(ParseError::Internal("lex mode stack underflow"));
        return;
    }
    let new_lex_mode = env.lex_mode();
    env.lookahead.reset(new_lex_mode);
}
Defensive patterns

Strategy: try-catch

Try / catch

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| parse(src)));
if result.is_err() {
    // record input for a parser bug report; skip file, keep the batch alive
    report_and_skip(file);
}

Prevention

When it happens

Trigger: A parse path where pop_lex_mode runs without a matching push_lex_mode: typically an error-recovery branch that pops after the success path already popped, or a rescan path (rescan_as_template_from) that rewinds the lexer cursor without restoring the mode stack. Line 2551 is pop_lex_mode itself.

Common situations: Unbalanced push/pop introduced while porting or refactoring parser rules; inputs mixing template literals, JSX, or type annotations that exercise rescanning; fuzzing the parser with malformed nested syntax.

Related errors


AI-assisted analysis of facebook/flow@5c86586199 (2026-08-20). Data as JSON: /api/errors/404b0344391c91a6. Report an issue: GitHub.