{"record":{"id":"ea1ba44d8e617ac1","repo":"twigphp/Twig","slug":"cannot-pop-state-without-a-previous-state","errorCode":null,"errorMessage":"Cannot pop state without a previous state.","messagePattern":"Cannot pop state without a previous state\\.","errorType":"exception","errorClass":"LogicException","httpStatus":null,"severity":"error","filePath":"src/Lexer.php","lineNumber":656,"sourceCode":"            // an operator with a space can be any amount of whitespaces\n            $r = preg_replace('/\\s+/', '\\s+', $r);\n\n            $regex[] = $r;\n        }\n\n        return '/'.implode('|', $regex).'/A';\n    }\n\n    private function pushState($state): void\n    {\n        $this->states[] = $this->state;\n        $this->state = $state;\n    }\n\n    private function popState(): void\n    {\n        if (0 === \\count($this->states)) {\n            throw new \\LogicException('Cannot pop state without a previous state.');\n        }\n\n        $this->state = array_pop($this->states);\n    }\n\n    private function checkBrackets(string $code): void\n    {\n        // opening bracket\n        if (\\in_array($code, $this->openingBrackets, true)) {\n            $this->brackets[] = [$code, $this->lineno];\n        } elseif (\\in_array($code, $this->closingBrackets, true)) {\n            // closing bracket\n            if (!$this->brackets) {\n                throw new SyntaxError(\\sprintf('Unexpected \"%s\".', $code), $this->lineno, $this->source, columnno: $this->source->getColumn($this->cursor));\n            }\n\n            [$expect, $lineno] = array_pop($this->brackets);\n            if ($code !== str_replace($this->openingBrackets, $this->closingBrackets, $expect)) {","sourceCodeStart":638,"sourceCodeEnd":674,"githubUrl":"https://github.com/twigphp/Twig/blob/a414c3a491defb5a60f2fc88ef79ff37c90010cd/src/Lexer.php#L638-L674","documentation":"The Lexer maintains a stack of lexer states (pushState/popState) as it tokenizes templates. popState is called when a construct like a closing bracket, string end, or interpolation end is reached; if the state stack is empty there is nothing to pop, meaning the lexer's state machine is out of sync — usually a lexer state was popped more times than pushed, or never pushed.","triggerScenarios":"Calling the protected popState path via a custom lexer subclass that pops without a matching pushState; a lexer state function (lexBlock, lexVar, lexString, lexInterpolation) reaching an exit condition that pops state when no state was pushed (e.g. malformed template input like an unmatched closing delimiter the push logic never opened).","commonSituations":"Custom Twig lexer extensions or custom delimiters whose begin/end logic is asymmetric; corrupted template syntax that drives the lexer into an unexpected transition; a bug in a forked Lexer where the initial state isn't pushed before the first pop.","solutions":["Search your code for calls to pushState/popState on the lexer and ensure every popState has a matching earlier pushState.","If you have a custom Lexer or expression parser changing lexer behavior, verify the state stack starts balanced: push the base state before lexing begins.","Reduce the template that triggers it to a minimal snippet and check for unmatched closing delimiters (}} , %}, ]), strings, or interpolation ends.","If the trigger is in a third-party lexer extension, update it or report the unbalanced state transition to its maintainer; as a last resort catch the LogicException at render/lex time to fail gracefully."],"exampleFix":"// before (custom lexer code)\nif ($this->punctuation === ']') { $this->popState(); }\n// after — only pop when a state was actually pushed\nif ($this->punctuation === ']' && $this->hasPushedState) { $this->hasPushedState = false; $this->popState(); }","handlingStrategy":"type-guard","validationCode":"if (0 === count($lexer->getStateStack())) { // expose or track your own mirror of pushed states\n    return; // nothing to pop\n}","typeGuard":"function canPopState(array $states): bool { return count($states) > 0; }","tryCatchPattern":"try {\n    $lexer->popState();\n} catch (\\LogicException $e) {\n    // log unbalanced lexer state; reset lexer state stack and abort tokenizing this template\n}","preventionTips":["Pair every pushState with exactly one popState in custom lexer code, ideally in try/finally.","Track pushed-state count yourself when subclassing the Lexer.","Test custom lexers with malformed templates (unmatched closers) to surface unbalanced transitions early."],"tags":["php","twig","lexer","state-machine","internal"],"backgroundTag":"invalid-state-transition","analyzedSha":"a414c3a491defb5a60f2fc88ef79ff37c90010cd","analyzedAt":"2026-09-13T15:10:46.849Z","contentChangedAt":"2026-09-13T15:10:46.849Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}