{"record":{"id":"404b0344391c91a6","repo":"facebook/flow","slug":"popping-lex-mode-from-empty-stack","errorCode":null,"errorMessage":"Popping lex mode from empty stack","messagePattern":"Popping lex mode from empty stack","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"rust_port/crates/flow_parser/src/parser_env.rs","lineNumber":2562,"sourceCode":"    /* [maybe env t] eats the next token and returns [true] if it is [t], else return [false] */\n    pub(crate) fn maybe(env: &mut ParserEnv, t: TokenKind) -> Result<bool, Rollback> {\n        let is_t = peek::token(env) == &t;\n        if is_t {\n            token(env)?;\n        }\n        Ok(is_t)\n    }\n\n    pub(crate) fn push_lex_mode(env: &mut ParserEnv, mode: LexMode) {\n        env.lex_mode_stack.push(mode);\n        let new_lex_mode = env.lex_mode();\n        env.lookahead.reset(new_lex_mode);\n    }\n\n    pub(crate) fn pop_lex_mode(env: &mut ParserEnv) {\n        env.lex_mode_stack\n            .pop()\n            .expect(\"Popping lex mode from empty stack\");\n        let new_lex_mode = env.lex_mode();\n        env.lookahead.reset(new_lex_mode);\n    }\n\n    pub(crate) fn double_pop_lex_mode(env: &mut ParserEnv) {\n        env.lex_mode_stack\n            .pop()\n            .expect(\"Popping lex mode from empty stack\");\n        env.lex_mode_stack\n            .pop()\n            .expect(\"Popping lex mode from empty stack\");\n        let new_lex_mode = env.lex_mode();\n        env.lookahead.reset(new_lex_mode);\n    }\n\n    pub(crate) fn rescan_as_template_from<'a>(\n        env: &mut ParserEnv<'a>,\n        prev_cursor: wrapped_lex_env::WrappedLexCursor,","sourceCodeStart":2544,"sourceCodeEnd":2580,"githubUrl":"https://github.com/facebook/flow/blob/5c865861998a8ccb7dbc82b0c1f511e9ef60c3d9/rust_port/crates/flow_parser/src/parser_env.rs#L2544-L2580","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Capture the exact panicking input and stack trace; identify the parse rule owning the pop_lex_mode call site","Audit that rule for a push_lex_mode without a matching pop on every path, especially error/rollback paths","Compare against upstream Flow parser behavior on the same input to see which side is unbalanced","Upgrade flow_parser; lexer-mode balancing regressions are usually fixed quickly after fuzz reports","If you maintain the code, use a checked pop that records an internal parse error instead of panicking"],"exampleFix":"// before: blind pop panics on underflow\npub(crate) fn pop_lex_mode(env: &mut ParserEnv) {\n    env.lex_mode_stack.pop().expect(\"Popping lex mode from empty stack\");\n    let new_lex_mode = env.lex_mode();\n    env.lookahead.reset(new_lex_mode);\n}\n\n// after: checked pop surfaces an internal error instead of crashing\npub(crate) fn pop_lex_mode(env: &mut ParserEnv) {\n    if env.lex_mode_stack.pop().is_none() {\n        env.error_at_current_loc(ParseError::Internal(\"lex mode stack underflow\"));\n        return;\n    }\n    let new_lex_mode = env.lex_mode();\n    env.lookahead.reset(new_lex_mode);\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| parse(src)));\nif result.is_err() {\n    // record input for a parser bug report; skip file, keep the batch alive\n    report_and_skip(file);\n}","preventionTips":["Fuzz the parser on upgrades, especially with template literals, JSX, and type annotations","If maintaining parser rules, assert lex_mode_stack depth in debug builds around push/pop pairs","Keep panicking inputs as regression tests"],"tags":["flow-parser","lexer","stack-underflow","panic","lex-mode"],"backgroundTag":"stack-underflow","analyzedSha":"5c865861998a8ccb7dbc82b0c1f511e9ef60c3d9","analyzedAt":"2026-08-20T10:41:37.992Z","contentChangedAt":"2026-08-20T10:41:37.992Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}