Skip to main content

rustpython_vm/vm/
vm_new.rs

1#[cfg(feature = "parser")]
2use ruff_python_ast::token::TokenKind;
3
4use ruff_python_parser::{InterpolatedStringErrorType, LexicalErrorType, ParseErrorType};
5
6use rustpython_common::wtf8::Wtf8Buf;
7use rustpython_compiler_core::SourceLocation;
8
9use core::ops::RangeInclusive;
10
11#[cfg(feature = "parser")]
12use rustpython_compiler::{CompileError, ParseError, is_blank_python_source};
13
14use crate::{
15    AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult,
16    builtins::{
17        PyAttributeError, PyBaseException, PyBaseExceptionRef, PyBytesRef, PyDictRef,
18        PyImportError, PyMemoryError, PyModule, PyNameError, PyOSError, PyStopIteration, PyStrRef,
19        PySyntaxError, PySystemExit, PyType, PyTypeRef,
20        builtin_func::PyNativeFunction,
21        descriptor::PyMethodDescriptor,
22        tuple::{IntoPyTuple, PyTupleRef},
23    },
24    convert::{ToPyException, ToPyObject},
25    exceptions::OSErrorBuilder,
26    function::{
27        FuncArgs, IntoPyNativeFn, PyMethodFlags, arity_message, unexpected_keyword_message,
28    },
29    scope::Scope,
30    set_attrs,
31    types::{Constructor, Initializer},
32    vm::VirtualMachine,
33};
34
35macro_rules! define_exception_fn {
36    (
37        fn $fn_name:ident, $attr:ident, $python_repr:ident
38    ) => {
39        #[doc = concat!(
40                    "Create a new python ",
41                    stringify!($python_repr),
42                    " object.\nUseful for raising errors from python functions implemented in rust."
43                )]
44        pub fn $fn_name(&self, msg: impl Into<Wtf8Buf>) -> $crate::builtins::PyBaseExceptionRef {
45            let err = self.ctx.exceptions.$attr.to_owned();
46            self.new_simple_exception(err, vec![self.ctx.new_str(msg.into()).into()])
47        }
48    };
49}
50
51#[derive(Clone, Debug)]
52struct SyntaxErrorInfo {
53    msg: String,
54    narrow_caret: bool,
55}
56
57impl SyntaxErrorInfo {
58    #[must_use]
59    const fn new(msg: String, narrow_caret: bool) -> Self {
60        Self { msg, narrow_caret }
61    }
62
63    #[cfg(feature = "parser")]
64    #[must_use]
65    const fn handle_expected_token(expected: TokenKind, found: TokenKind) -> &'static str {
66        match (expected, found) {
67            (TokenKind::Colon, TokenKind::Newline) => "expected ':'",
68
69            (TokenKind::Lpar, _) => "expected '('",
70
71            (TokenKind::Else, y) if !matches!(y, TokenKind::Colon) => {
72                "expected 'else' after 'if' expression"
73            }
74
75            _ => "invalid syntax",
76        }
77    }
78
79    #[cfg(feature = "parser")]
80    fn analyze_compile_error(&mut self, compile_error: &CompileError) {
81        let CompileError::Parse(ParseError {
82            error, location, ..
83        }) = compile_error
84        else {
85            return;
86        };
87
88        let msg = match error {
89            ParseErrorType::FStringError(InterpolatedStringErrorType::UnterminatedString)
90            | ParseErrorType::Lexical(LexicalErrorType::FStringError(
91                InterpolatedStringErrorType::UnterminatedString,
92            )) => "unterminated f-string literal".into(),
93
94            ParseErrorType::TStringError(InterpolatedStringErrorType::UnterminatedString)
95            | ParseErrorType::Lexical(LexicalErrorType::TStringError(
96                InterpolatedStringErrorType::UnterminatedString,
97            )) => "unterminated t-string literal".into(),
98
99            ParseErrorType::FStringError(
100                InterpolatedStringErrorType::UnterminatedTripleQuotedString,
101            )
102            | ParseErrorType::Lexical(LexicalErrorType::FStringError(
103                InterpolatedStringErrorType::UnterminatedTripleQuotedString,
104            )) => "unterminated triple-quoted f-string literal".into(),
105
106            ParseErrorType::TStringError(
107                InterpolatedStringErrorType::UnterminatedTripleQuotedString,
108            )
109            | ParseErrorType::Lexical(LexicalErrorType::TStringError(
110                InterpolatedStringErrorType::UnterminatedTripleQuotedString,
111            )) => "unterminated triple-quoted t-string literal".into(),
112
113            // ruff already prefixes these with `f-string: ` / `t-string: `, matching CPython.
114            // It quotes braces with backticks where CPython uses single quotes.
115            ParseErrorType::FStringError(_)
116            | ParseErrorType::TStringError(_)
117            | ParseErrorType::Lexical(
118                LexicalErrorType::FStringError(_) | LexicalErrorType::TStringError(_),
119            ) => self.msg.replace('`', "'"),
120
121            ParseErrorType::UnexpectedExpressionToken => "invalid syntax".into(),
122
123            ParseErrorType::ExpectedToken { expected, found } => {
124                Self::handle_expected_token(*expected, *found).into()
125            }
126
127            ParseErrorType::InvalidStarredExpressionUsage => {
128                self.narrow_caret = true;
129                "invalid syntax".into()
130            }
131
132            ParseErrorType::InvalidDeleteTarget => "invalid syntax".into(),
133
134            ParseErrorType::Lexical(LexicalErrorType::LineContinuationError) => {
135                "unexpected character after line continuation character".into()
136            }
137
138            ParseErrorType::Lexical(LexicalErrorType::UnclosedStringError) => {
139                format!(
140                    "unterminated string literal (detected at line {})",
141                    location.line
142                )
143            }
144
145            ParseErrorType::EmptyTypeParams => "Type parameter list cannot be empty".into(),
146
147            ParseErrorType::InvalidStarPatternUsage => {
148                self.narrow_caret = true;
149                "cannot use starred expression here".into()
150            }
151
152            ParseErrorType::ExpectedKeywordParam => "named arguments must follow bare *".into(),
153
154            ParseErrorType::EmptyImportNames => "Expected one or more names after 'import'".into(),
155
156            ParseErrorType::UnparenthesizedGeneratorExpression => {
157                "Generator expression must be parenthesized".into()
158            }
159
160            ParseErrorType::NonDefaultParamAfterDefaultParam => {
161                "parameter without a default follows parameter with a default".into()
162            }
163
164            ParseErrorType::VarParameterWithDefault => {
165                "var-positional argument cannot have default value".into()
166            }
167
168            ParseErrorType::PositionalAfterKeywordArgument => {
169                "positional argument follows keyword argument".into()
170            }
171
172            ParseErrorType::PositionalAfterKeywordUnpacking => {
173                "positional argument follows keyword argument unpacking".into()
174            }
175
176            ParseErrorType::InvalidArgumentUnpackingOrder => {
177                "iterable argument unpacking follows keyword argument unpacking".into()
178            }
179
180            ParseErrorType::ParamAfterVarKeywordParam => {
181                "arguments cannot follow var-keyword argument".into()
182            }
183
184            ParseErrorType::InvalidAnnotatedAssignmentTarget => {
185                "illegal target for annotation".into()
186            }
187
188            ParseErrorType::Lexical(LexicalErrorType::UnrecognizedToken { .. })
189            | ParseErrorType::SimpleStatementsOnSameLine
190            | ParseErrorType::SimpleAndCompoundStatementOnSameLine
191            | ParseErrorType::ExpectedExpression => "invalid syntax".into(),
192
193            ParseErrorType::OtherError(s) if s.starts_with("Expected an identifier") => {
194                "invalid syntax".into()
195            }
196
197            // What the parser says when it cannot continue the list it is
198            // recovering; each of these situations is a plain "invalid syntax".
199            ParseErrorType::OtherError(s)
200                if matches!(
201                    s.as_str(),
202                    "Expected a statement"
203                        | "Expected an `elif` or `else` clause, or the end of the `if` statement."
204                        | "Expected an `except` or `finally` clause or the end of the `try` statement."
205                        | "The keyword is not allowed as a variable declaration name"
206                        | "Expected an assignment target"
207                        | "Expected a type parameter or the end of the type parameter list"
208                        | "Expected an import name or a ')'"
209                        | "Expected an import name"
210                        | "Expected an expression or the end of the slice list"
211                        | "Expected an expression or a ']'"
212                        | "Expected an expression or a '}'"
213                        | "Expected an expression or a ')'"
214                        | "Expected an expression"
215                        | "Expected a pattern or the end of the sequence pattern"
216                        | "Expected a mapping pattern or the end of the mapping pattern"
217                        | "Expected a pattern or a ')'"
218                        | "Expected a delete target"
219                        | "Expected a parameter or the end of the parameter list"
220                        | "Expected an expression or the end of the with item list"
221                ) =>
222            {
223                "invalid syntax".into()
224            }
225
226            ParseErrorType::OtherError(s)
227                if s.eq_ignore_ascii_case(
228                    "bytes literal cannot be mixed with non-bytes literals",
229                ) =>
230            {
231                "cannot mix bytes and nonbytes literals".into()
232            }
233
234            ParseErrorType::OtherError(s)
235                if s.eq_ignore_ascii_case("positional patterns cannot follow keyword patterns") =>
236            {
237                "positional patterns follow keyword patterns".into()
238            }
239
240            ParseErrorType::OtherError(s)
241                if s.eq_ignore_ascii_case("boolean 'not' expression cannot be used here") =>
242            {
243                "'not' after an operator must be parenthesized".into()
244            }
245
246            ParseErrorType::OtherError(s)
247                if s.eq_ignore_ascii_case("trailing comma not allowed") =>
248            {
249                "trailing comma not allowed without surrounding parentheses".into()
250            }
251
252            ParseErrorType::OtherError(s)
253                if s.eq_ignore_ascii_case(
254                    "multiple exception types must be parenthesized when using `as`",
255                ) =>
256            {
257                "multiple exception types must be parenthesized when using 'as'".into()
258            }
259
260            ParseErrorType::OtherError(s)
261                if s.eq_ignore_ascii_case(
262                    "position-only parameter separator not allowed as first parameter",
263                ) =>
264            {
265                "at least one argument must precede /".into()
266            }
267
268            ParseErrorType::OtherError(s)
269                if s.eq_ignore_ascii_case("only one '/' separator allowed") =>
270            {
271                "/ may appear only once".into()
272            }
273
274            ParseErrorType::OtherError(s)
275                if s.eq_ignore_ascii_case("'/' parameter must appear before '*' parameter") =>
276            {
277                "/ must be ahead of *".into()
278            }
279
280            ParseErrorType::OtherError(s)
281                if s.eq_ignore_ascii_case("expected `except` or `finally` after `try` block") =>
282            {
283                "expected 'except' or 'finally' block".into()
284            }
285
286            ParseErrorType::OtherError(s)
287                if s.eq_ignore_ascii_case("only one '*' parameter allowed") =>
288            {
289                "* argument may appear only once".into()
290            }
291
292            ParseErrorType::OtherError(s)
293                if s.eq_ignore_ascii_case(
294                    r#"cannot have both 'except' and 'except*' on the same 'try'"#,
295                ) =>
296            {
297                r#"cannot have both 'except' and 'except*' on the same 'try'"#.into()
298            }
299
300            _ => return,
301        };
302
303        self.msg = msg;
304    }
305}
306
307/// Collection of object creation helpers
308impl VirtualMachine {
309    /// Create a new python object
310    pub fn new_pyobj(&self, value: impl ToPyObject) -> PyObjectRef {
311        value.to_pyobject(self)
312    }
313
314    pub fn new_tuple(&self, value: impl IntoPyTuple) -> PyTupleRef {
315        value.into_pytuple(self)
316    }
317
318    pub fn new_module(
319        &self,
320        name: &str,
321        dict: PyDictRef,
322        doc: Option<PyStrRef>,
323    ) -> PyRef<PyModule> {
324        let module = PyRef::new_ref(
325            PyModule::new(),
326            self.ctx.types.module_type.to_owned(),
327            Some(dict),
328        );
329        module.init_dict(self.ctx.intern_str(name), doc, self);
330        module
331    }
332
333    pub fn new_scope_with_builtins(&self) -> Scope {
334        Scope::with_builtins(None, self.ctx.new_dict(), self)
335    }
336
337    pub fn new_scope_with_main(&self) -> PyResult<Scope> {
338        let scope = self.new_scope_with_builtins();
339        let main_module = self.new_module("__main__", scope.globals.clone(), None);
340
341        self.sys_module.get_attr("modules", self)?.set_item(
342            "__main__",
343            main_module.into(),
344            self,
345        )?;
346        self.set_main_builtin_importer(&scope.globals)?;
347
348        Ok(scope)
349    }
350
351    /// Create `__main__` if it is missing and return it.
352    pub fn ensure_main_module(&self) -> PyResult<PyRef<PyModule>> {
353        let sys_modules = self.sys_module.get_attr("modules", self)?;
354        let module = if let Ok(existing) = sys_modules.get_item("__main__", self)
355            && let Ok(module) = existing.downcast::<PyModule>()
356        {
357            module
358        } else {
359            let dict = self.ctx.new_dict();
360            let main_module = self.new_module("__main__", dict, None);
361            sys_modules.set_item("__main__", main_module.clone().into(), self)?;
362            main_module
363        };
364        self.set_main_builtin_importer(&module.dict())?;
365        Ok(module)
366    }
367
368    /// Set `__main__.__loader__` to BuiltinImporter when it is missing or None.
369    pub fn set_main_builtin_importer(
370        &self,
371        module_dict: &Py<crate::builtins::PyDict>,
372    ) -> PyResult<()> {
373        if let Ok(loader) = module_dict.get_item("__loader__", self)
374            && !self.is_none(&loader)
375        {
376            return Ok(());
377        }
378        let sys_modules = self.sys_module.get_attr("modules", self)?;
379        let Ok(importlib) = sys_modules.get_item("_frozen_importlib", self) else {
380            return Ok(());
381        };
382        let loader = importlib.get_attr("BuiltinImporter", self)?;
383        module_dict.set_item("__loader__", loader, self)?;
384        Ok(())
385    }
386
387    /// `__dict__` of this interpreter's `__main__` module.
388    pub fn main_namespace(&self) -> PyResult<PyDictRef> {
389        let main = self.ensure_main_module()?;
390        Ok(main.dict())
391    }
392
393    pub fn new_function<F, FKind>(&self, name: &'static str, f: F) -> PyRef<PyNativeFunction>
394    where
395        F: IntoPyNativeFn<FKind>,
396    {
397        let def = self.ctx.new_method_def(
398            name,
399            f,
400            PyMethodFlags::empty(),
401            crate::function::ItemDoc::NONE,
402        );
403        def.build_function(self, None)
404    }
405
406    pub fn new_method<F, FKind>(
407        &self,
408        name: &'static str,
409        class: &'static Py<PyType>,
410        f: F,
411    ) -> PyRef<PyMethodDescriptor>
412    where
413        F: IntoPyNativeFn<FKind>,
414    {
415        let def = self.ctx.new_method_def(
416            name,
417            f,
418            PyMethodFlags::METHOD,
419            crate::function::ItemDoc::NONE,
420        );
421        def.build_method(class, self)
422    }
423
424    /// Allocate a `BaseException`-layout instance without running `__new__`/`__init__`.
425    ///
426    /// Only valid when `exc_type` uses the `BaseException` payload. Extra-payload
427    /// types (OSError, UnicodeError, ExceptionGroup, SystemExit, ...) must use
428    /// [`new_exception`][Self::new_exception] or
429    /// [`invoke_exception`][Self::invoke_exception].
430    pub fn new_simple_exception(
431        &self,
432        exc_type: PyTypeRef,
433        args: Vec<PyObjectRef>,
434    ) -> PyBaseExceptionRef {
435        debug_assert_eq!(
436            exc_type.slots.basicsize,
437            crate::object::SIZEOF_PYOBJECT_HEAD + core::mem::size_of::<PyBaseException>(),
438            "vm.new_simple_exception() requires a BaseException-sized type, got {}",
439            exc_type.name()
440        );
441        PyBaseException::new(args, self)
442            .into_ref_with_type_lazy_dict(self, exc_type)
443            .expect("vm.new_simple_exception() called with an invalid exception type")
444    }
445
446    /// Instantiate an exception with arguments.
447    ///
448    /// `BaseException`-sized types use
449    /// [`new_simple_exception`][Self::new_simple_exception]. Extra-payload types
450    /// run the type constructor so their fields are initialized. User-defined
451    /// types that override `__new__`/`__init__` should still use
452    /// [`invoke_exception`][Self::invoke_exception] or
453    /// [`exceptions::ExceptionCtor`][crate::exceptions::ExceptionCtor].
454    pub fn new_exception(&self, exc_type: PyTypeRef, args: Vec<PyObjectRef>) -> PyBaseExceptionRef {
455        if exc_type.slots.basicsize
456            == crate::object::SIZEOF_PYOBJECT_HEAD + core::mem::size_of::<PyBaseException>()
457        {
458            self.new_simple_exception(exc_type, args)
459        } else {
460            self.invoke_exception(&exc_type, args).unwrap_or_else(|e| e)
461        }
462    }
463
464    /// Construct a built-in exception type that carries a payload, directly
465    /// (`py_new` + `slot_init`), without routing through `PyType::call`.
466    /// Only valid for a built-in `T` whose exact type is known at compile time.
467    pub fn new_payload_exception<T>(&self, cls: PyTypeRef, args: FuncArgs) -> PyResult<PyRef<T>>
468    where
469        T: Constructor<Args = FuncArgs> + Initializer,
470    {
471        debug_assert_eq!(
472            cls.slots.basicsize,
473            crate::object::SIZEOF_PYOBJECT_HEAD + size_of::<T>(),
474            "vm.new_payload_exception::<{}>() called with mismatched type '{}'",
475            core::any::type_name::<T>(),
476            cls.name()
477        );
478        let obj = T::slot_new(cls, args.clone(), self)?;
479        T::slot_init(&obj, args, self)?;
480        obj.downcast().map_err(|obj| {
481            self.new_type_error(format!(
482                "payload constructor returned '{}'",
483                obj.class().name()
484            ))
485        })
486    }
487
488    pub fn new_os_error(&self, msg: impl ToPyObject) -> PyRef<PyBaseException> {
489        self.new_os_subtype_error(self.ctx.exceptions.os_error.to_owned(), None, msg)
490            .upcast()
491    }
492
493    pub fn new_os_subtype_error(
494        &self,
495        exc_type: PyTypeRef,
496        errno: Option<i32>,
497        msg: impl ToPyObject,
498    ) -> PyRef<PyOSError> {
499        debug_assert_eq!(
500            exc_type.slots.basicsize,
501            crate::object::SIZEOF_PYOBJECT_HEAD + core::mem::size_of::<PyOSError>()
502        );
503
504        OSErrorBuilder::with_subtype(exc_type, errno, msg, self).build(self)
505    }
506
507    /// Instantiate an exception with no arguments.
508    pub fn new_exception_empty(&self, exc_type: PyTypeRef) -> PyBaseExceptionRef {
509        self.new_exception(exc_type, vec![])
510    }
511
512    /// Instantiate an exception with `msg` as the only argument.
513    pub fn new_exception_msg(&self, exc_type: PyTypeRef, msg: Wtf8Buf) -> PyBaseExceptionRef {
514        self.new_exception(exc_type, vec![self.ctx.new_str(msg).into()])
515    }
516
517    /// Instantiate an exception with `msg` as the only argument and `dict` for object
518    /// This function should only be used with builtin exception types; if a user-defined exception
519    /// type is passed in, it may not be fully initialized; try using
520    /// [`vm.invoke_exception()`][Self::invoke_exception] or
521    /// [`exceptions::ExceptionCtor`][crate::exceptions::ExceptionCtor] instead.
522    pub fn new_exception_msg_dict(
523        &self,
524        exc_type: PyTypeRef,
525        msg: Wtf8Buf,
526        dict: PyDictRef,
527    ) -> PyBaseExceptionRef {
528        PyRef::new_ref(
529            // TODO: this constructor might be invalid, because multiple
530            // exception (even builtin ones) are using custom constructors,
531            // see `OSError` as an example:
532            PyBaseException::new(vec![self.ctx.new_str(msg).into()], self),
533            exc_type,
534            Some(dict),
535        )
536    }
537
538    pub fn new_no_attribute_error(&self, obj: PyObjectRef, name: PyStrRef) -> PyBaseExceptionRef {
539        let msg = format!(
540            "'{}' object has no attribute '{}'",
541            obj.class().slot_name(),
542            name
543        );
544        let attribute_error = self.new_attribute_error(msg);
545
546        // Use existing set_attribute_error_context function
547        self.set_attribute_error_context(&attribute_error, obj, name);
548
549        attribute_error
550    }
551
552    pub fn new_name_error(&self, msg: impl Into<Wtf8Buf>, name: PyStrRef) -> PyBaseExceptionRef {
553        let name_error = self
554            .new_payload_exception::<PyNameError>(
555                self.ctx.exceptions.name_error.to_owned(),
556                vec![self.ctx.new_str(msg.into()).into()].into(),
557            )
558            .expect("NameError construction from internal args is infallible");
559        set_attrs!(
560            name_error.as_object(), self, unwrap,
561            "name" => name,
562        );
563
564        name_error.upcast()
565    }
566
567    /// The error a call that passed the wrong number of arguments gets. The
568    /// name is the function the call was for; `Self::NAME` is the one a slot
569    /// wants. `_PyArg_CheckPositional`
570    pub fn new_arity_type_error(
571        &self,
572        func_name: &str,
573        arity: RangeInclusive<usize>,
574        num_given: usize,
575    ) -> PyBaseExceptionRef {
576        let too_few = num_given < *arity.start();
577        self.new_type_error(arity_message(Some(func_name), &arity, too_few, num_given))
578    }
579
580    /// The error a call that passed a keyword the function doesn't take gets.
581    /// The name is left off where the parser has none of its own, the way
582    /// `_PyArg_Parser.fname` is NULL. `_PyArg_UnpackKeywords`
583    pub fn new_unexpected_keyword_type_error(
584        &self,
585        func_name: Option<&str>,
586        keyword: &str,
587    ) -> PyBaseExceptionRef {
588        self.new_type_error(unexpected_keyword_message(func_name, keyword))
589    }
590
591    pub fn new_unsupported_unary_error(&self, a: &PyObject, op: &str) -> PyBaseExceptionRef {
592        self.new_type_error(format!(
593            "bad operand type for {}: '{}'",
594            op,
595            a.class().slot_name()
596        ))
597    }
598
599    pub fn new_unsupported_bin_op_error(
600        &self,
601        a: &PyObject,
602        b: &PyObject,
603        op: &str,
604    ) -> PyBaseExceptionRef {
605        self.new_type_error(format!(
606            "unsupported operand type(s) for {}: '{}' and '{}'",
607            op,
608            a.class().slot_name(),
609            b.class().slot_name()
610        ))
611    }
612
613    pub fn new_unsupported_ternary_op_error(
614        &self,
615        a: &PyObject,
616        b: &PyObject,
617        c: &PyObject,
618        op: &str,
619    ) -> PyBaseExceptionRef {
620        self.new_type_error(format!(
621            "unsupported operand type(s) for {}: '{}', '{}', '{}'",
622            op,
623            a.class().slot_name(),
624            b.class().slot_name(),
625            c.class().slot_name()
626        ))
627    }
628
629    /// Create a new OSError from the last OS error.
630    ///
631    /// On windows, windows-sys errors are expected to be handled by this function.
632    /// This is identical to `new_last_errno_error` on non-Windows platforms.
633    pub fn new_last_os_error(&self) -> PyBaseExceptionRef {
634        let err = std::io::Error::last_os_error();
635        err.to_pyexception(self)
636    }
637
638    /// Create a new OSError from the last POSIX errno.
639    ///
640    /// On windows, CRT errno are expected to be handled by this function.
641    /// This is identical to `new_last_os_error` on non-Windows platforms.
642    pub fn new_last_errno_error(&self) -> PyBaseExceptionRef {
643        let err = crate::host_env::os::errno_io_error();
644        err.to_pyexception(self)
645    }
646
647    pub fn new_errno_error(&self, errno: i32, msg: impl ToPyObject) -> PyRef<PyOSError> {
648        let exc_type = crate::exceptions::errno_to_exc_type(errno, self)
649            .unwrap_or(self.ctx.exceptions.os_error);
650
651        self.new_os_subtype_error(exc_type.to_owned(), Some(errno), msg)
652    }
653
654    pub fn new_unicode_decode_error(
655        &self,
656        encoding: PyStrRef,
657        object: PyBytesRef,
658        start: usize,
659        end: usize,
660        reason: PyStrRef,
661    ) -> PyBaseExceptionRef {
662        let start = self.ctx.new_int(start);
663        let end = self.ctx.new_int(end);
664        self.invoke_exception(
665            self.ctx.exceptions.unicode_decode_error,
666            vec![
667                encoding.into(),
668                object.into(),
669                start.into(),
670                end.into(),
671                reason.into(),
672            ],
673        )
674        .expect("UnicodeDecodeError constructor")
675    }
676
677    pub fn new_unicode_encode_error(
678        &self,
679        encoding: PyStrRef,
680        object: PyStrRef,
681        start: usize,
682        end: usize,
683        reason: PyStrRef,
684    ) -> PyBaseExceptionRef {
685        let start = self.ctx.new_int(start);
686        let end = self.ctx.new_int(end);
687        self.invoke_exception(
688            self.ctx.exceptions.unicode_encode_error,
689            vec![
690                encoding.into(),
691                object.into(),
692                start.into(),
693                end.into(),
694                reason.into(),
695            ],
696        )
697        .expect("UnicodeEncodeError constructor")
698    }
699
700    #[cfg(feature = "parser")]
701    fn source_has_mixed_tabs_and_spaces(source: Option<&str>, error_line: usize) -> bool {
702        source.is_some_and(|source| {
703            let mut has_space_indent = false;
704            let mut has_tab_indent = false;
705            for (i, line) in source.lines().enumerate() {
706                if i + 1 > error_line {
707                    break;
708                }
709                let rest = line.trim_start_matches([' ', '\t']);
710                // Blank and comment-only lines do not participate in indent.
711                if rest.is_empty() || rest.starts_with('#') {
712                    continue;
713                }
714                let indent = &line.as_bytes()[..line.len() - rest.len()];
715                if indent.is_empty() {
716                    continue;
717                }
718                if indent.contains(&b' ') && indent.contains(&b'\t') {
719                    return true;
720                }
721                if indent.contains(&b' ') {
722                    has_space_indent = true;
723                }
724                if indent.contains(&b'\t') {
725                    has_tab_indent = true;
726                }
727            }
728            has_space_indent && has_tab_indent
729        })
730    }
731
732    // TODO: don't take ownership should make the success path faster
733    pub fn new_key_error(&self, obj: PyObjectRef) -> PyBaseExceptionRef {
734        let key_error = self.ctx.exceptions.key_error.to_owned();
735        self.new_simple_exception(key_error, vec![obj])
736    }
737
738    #[cfg(any(feature = "parser", feature = "compiler"))]
739    pub fn new_syntax_error_maybe_incomplete(
740        &self,
741        error: &crate::compiler::CompileError,
742        source: Option<&str>,
743        allow_incomplete: bool,
744    ) -> PyBaseExceptionRef {
745        if matches!(
746            error,
747            crate::compiler::CompileError::Codegen(crate::compiler::codegen::error::CodegenError {
748                error: crate::compiler::codegen::error::CodegenErrorType::RecursionError,
749                ..
750            })
751        ) {
752            return self.new_recursion_error(error.to_string());
753        }
754
755        let incomplete_or_syntax = |allow| -> &'static Py<crate::builtins::PyType> {
756            if allow {
757                self.ctx.exceptions.incomplete_input_error
758            } else {
759                self.ctx.exceptions.syntax_error
760            }
761        };
762
763        let syntax_error_type = match &error {
764            #[cfg(feature = "parser")]
765            crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
766                error:
767                    ruff_python_parser::ParseErrorType::Lexical(
768                        ruff_python_parser::LexicalErrorType::IndentationError,
769                    )
770                    | ruff_python_parser::ParseErrorType::UnexpectedIndentation,
771                location,
772                ..
773            }) => {
774                if Self::source_has_mixed_tabs_and_spaces(source, location.line.get()) {
775                    self.ctx.exceptions.tab_error
776                } else {
777                    self.ctx.exceptions.indentation_error
778                }
779            }
780            #[cfg(feature = "parser")]
781            crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
782                error:
783                    ruff_python_parser::ParseErrorType::Lexical(
784                        ruff_python_parser::LexicalErrorType::Eof,
785                    ),
786                ..
787            }) => incomplete_or_syntax(allow_incomplete),
788            // Unclosed bracket errors (converted from Eof by from_ruff_parse_error)
789            #[cfg(feature = "parser")]
790            crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
791                is_unclosed_bracket: true,
792                ..
793            }) => incomplete_or_syntax(allow_incomplete),
794            #[cfg(feature = "parser")]
795            crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
796                is_unclosed_string: true,
797                ..
798            }) => incomplete_or_syntax(allow_incomplete),
799            #[cfg(feature = "parser")]
800            crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
801                error:
802                    ruff_python_parser::ParseErrorType::Lexical(
803                        ruff_python_parser::LexicalErrorType::FStringError(
804                            ruff_python_parser::InterpolatedStringErrorType::UnterminatedTripleQuotedString,
805                        )
806                        | ruff_python_parser::LexicalErrorType::TStringError(
807                            ruff_python_parser::InterpolatedStringErrorType::UnterminatedTripleQuotedString,
808                        ),
809                    ),
810                ..
811            }) => incomplete_or_syntax(allow_incomplete),
812            #[cfg(feature = "parser")]
813            crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
814                error:
815                    ruff_python_parser::ParseErrorType::Lexical(
816                        ruff_python_parser::LexicalErrorType::UnclosedStringError,
817                    ),
818                ..
819            }) => {
820                if allow_incomplete {
821                    incomplete_or_syntax(source.is_some_and(unclosed_string_is_incomplete))
822                } else {
823                    self.ctx.exceptions.syntax_error
824                }
825            }
826            #[cfg(feature = "parser")]
827            crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
828                error: ruff_python_parser::ParseErrorType::OtherError(s),
829                raw_location,
830                ..
831            }) => {
832                if s.starts_with("Expected an indented block after")
833                    || s.starts_with("expected an indented block after")
834                {
835                    if allow_incomplete {
836                        // Check that all chars in the error are whitespace, if so, the source is
837                        // incomplete. Otherwise, we've found code that might violates
838                        // indentation rules.
839                        let mut is_incomplete = true;
840                        if let Some(source) = source {
841                            let start = raw_location.start().to_usize();
842                            let end = raw_location.end().to_usize();
843                            let mut iter = source.chars();
844                            iter.nth(start);
845                            for _ in start..end {
846                                if let Some(c) = iter.next() {
847                                    if !c.is_ascii_whitespace() {
848                                        is_incomplete = false;
849                                    }
850                                } else {
851                                    break;
852                                }
853                            }
854                        }
855
856                        if is_incomplete {
857                            self.ctx.exceptions.incomplete_input_error
858                        } else {
859                            self.ctx.exceptions.indentation_error // not syntax_error
860                        }
861                    } else {
862                        self.ctx.exceptions.indentation_error
863                    }
864                } else if allow_incomplete
865                    && (s == "incomplete input"
866                        || (s == "unexpected EOF while parsing"
867                            && source.is_some_and(|source| {
868                                raw_location.end().to_usize() >= source.len()
869                                    && !source.ends_with('\n')
870                            })))
871                {
872                    self.ctx.exceptions.incomplete_input_error
873                } else {
874                    self.ctx.exceptions.syntax_error
875                }
876            }
877            _ => self.ctx.exceptions.syntax_error,
878        };
879        let syntax_error_type = if allow_incomplete && source.is_some_and(is_blank_python_source) {
880            self.ctx.exceptions.incomplete_input_error
881        } else {
882            syntax_error_type
883        }
884        .to_owned();
885
886        // TODO: replace to SourceCode
887        fn get_statement(source: &str, loc: Option<SourceLocation>) -> Option<String> {
888            let line = source
889                .split('\n')
890                .nth(loc?.line.to_zero_indexed())?
891                .trim_end_matches('\r')
892                .to_owned();
893            Some(line + "\n")
894        }
895
896        // Tokenizer/parser errors attach the current source line. Compiler
897        // errors load that line with ProgramText, which is None when the
898        // filename cannot be opened (compile of a string).
899        let mut statement = if matches!(error, crate::compiler::CompileError::Codegen(_)) {
900            self.program_text(error.source_path(), error.python_location().0)
901        } else {
902            source.and_then(|src| get_statement(src, error.location()))
903        };
904
905        let mut msg = error.to_string();
906        if !msg.starts_with("Exceeds the limit ")
907            && !msg.starts_with("Did you mean ")
908            && !msg.starts_with("Invalid star expression")
909            && !msg.starts_with("Function parameters cannot be parenthesized")
910            && !msg.starts_with("Lambda expression parameters cannot be parenthesized")
911            && !msg.starts_with("Cannot have two type comments on def")
912            && !msg.starts_with("Variable annotation syntax is")
913            && !msg.starts_with("The '@' operator is")
914            && !msg.starts_with("Async functions are")
915            && !msg.starts_with("Async comprehensions are")
916            && !msg.starts_with("Async for loops are")
917            && !msg.starts_with("Async with statements are")
918            && !msg.starts_with("Exception groups are")
919            && !msg.starts_with("Positional-only parameters are")
920            && !msg.starts_with("Pattern matching is")
921            && !msg.starts_with("Type statement is")
922            && !msg.starts_with("Type parameter lists are")
923            && !msg.starts_with("Type parameter defaults are")
924            && !msg.starts_with("Assignment expressions are")
925            && !msg.starts_with("Await expressions are")
926            && !msg.starts_with("Underscores in numeric literals are")
927            && !msg.starts_with("Missing parentheses")
928            && let Some(msg) = msg.get_mut(..1)
929        {
930            msg.make_ascii_lowercase();
931        }
932
933        cfg_select! {
934            feature = "parser" => {
935                let mut syntax_error_info = SyntaxErrorInfo::new(msg, false);
936                syntax_error_info.analyze_compile_error(error);
937            }
938            _ => {
939                let syntax_error_info = SyntaxErrorInfo::new(msg, false);
940            }
941        };
942
943        if syntax_error_type.is(self.ctx.exceptions.tab_error) {
944            syntax_error_info.msg =
945                String::from("inconsistent use of tabs and spaces in indentation");
946        } else if syntax_error_type.is(self.ctx.exceptions.incomplete_input_error) {
947            syntax_error_info.msg = String::from("incomplete input");
948        }
949
950        let SyntaxErrorInfo { msg, narrow_caret } = syntax_error_info;
951        let unterminated_triple_quoted_string = msg.starts_with("unterminated triple-quoted");
952        let unexpected_eof_error = msg == "unexpected EOF while parsing";
953        if unterminated_triple_quoted_string
954            && let Some(statement) = statement.as_mut()
955            && statement.ends_with('\n')
956        {
957            // CPython omits the parser-added final newline from SyntaxError.text.
958            statement.pop();
959        }
960        let check_version_suite_error = msg.starts_with("Async functions are")
961            || msg.starts_with("Async for loops are")
962            || msg.starts_with("Async with statements are")
963            || msg.starts_with("Exception groups are")
964            || msg.starts_with("except expressions without parentheses are")
965            || msg.starts_with("Pattern matching is");
966        let line_end_binary_operator_error = msg.starts_with("The '@' operator is");
967        let unclosed_bracket_error = cfg_select! {
968            feature = "parser" => {
969                matches!(
970                    error,
971                    crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
972                        is_unclosed_bracket: true,
973                        ..
974                    })
975                )
976            }
977            _ => false,
978        };
979
980        let syntax_error: PyBaseExceptionRef = self
981            .new_payload_exception::<PySyntaxError>(
982                syntax_error_type,
983                vec![self.ctx.new_str(msg).into()].into(),
984            )
985            .expect("SyntaxError construction from internal args is infallible")
986            .upcast();
987
988        let (lineno_raw, offset_raw) = error.python_location();
989        let lineno = self.ctx.new_int(lineno_raw);
990        let offset = self.ctx.new_int(offset_raw);
991        set_attrs!(
992            syntax_error.as_object(), self, unwrap,
993            "lineno" => lineno,
994            "offset" => offset,
995        );
996
997        // Set end_lineno and end_offset if available
998        if let Some((end_lineno, end_offset)) = error.python_end_location() {
999            // EOF errors have no source span in CPython.
1000            let no_end_offset = unexpected_eof_error
1001                || (check_version_suite_error
1002                    && statement
1003                        .as_deref()
1004                        .and_then(|line| line.chars().next())
1005                        .is_some_and(|ch| ch.is_ascii_whitespace()));
1006            let (end_lineno, end_offset) = if no_end_offset {
1007                (end_lineno, -1)
1008            } else if unclosed_bracket_error {
1009                // The bracket that was never closed is marked where it opened,
1010                // and the span stops there.
1011                (end_lineno, 0)
1012            } else if line_end_binary_operator_error && end_offset == offset_raw {
1013                (end_lineno, (end_offset + 1) as isize)
1014            } else if narrow_caret {
1015                let (l, o) = error.python_location();
1016                (l, (o + 1) as isize)
1017            } else {
1018                (end_lineno, end_offset as isize)
1019            };
1020            let end_lineno = self.ctx.new_int(end_lineno);
1021            let end_offset = self.ctx.new_int(end_offset);
1022            set_attrs!(
1023                syntax_error.as_object(), self, unwrap,
1024                "end_lineno" => end_lineno,
1025                "end_offset" => end_offset,
1026            );
1027        }
1028
1029        set_attrs!(
1030            syntax_error.as_object(), self, unwrap,
1031            "text" => statement.to_pyobject(self),
1032            "filename" => self.ctx.new_str(error.source_path())
1033        );
1034
1035        // Set _metadata for keyword typo suggestions in traceback module.
1036        // Format: (start_line, col_offset, source_code)
1037        // start_line=0 means "include all lines from beginning" which provides
1038        // full context needed by _find_keyword_typos to compile-check suggestions.
1039        if let Some(source) = source {
1040            let metadata = self.ctx.new_tuple(vec![
1041                self.ctx.new_int(0).into(),
1042                self.ctx.new_int(0).into(),
1043                self.ctx.new_str(source).into(),
1044            ]);
1045            set_attrs!(
1046                syntax_error.as_object(), self, unwrap,
1047                "_metadata" => metadata,
1048            );
1049        }
1050
1051        syntax_error
1052    }
1053
1054    #[cfg(any(feature = "parser", feature = "compiler"))]
1055    pub fn new_syntax_error(
1056        &self,
1057        error: &crate::compiler::CompileError,
1058        source: Option<&str>,
1059    ) -> PyBaseExceptionRef {
1060        self.new_syntax_error_maybe_incomplete(error, source, false)
1061    }
1062
1063    pub fn new_import_error(
1064        &self,
1065        msg: impl Into<Wtf8Buf>,
1066        name: impl Into<PyStrRef>,
1067    ) -> PyBaseExceptionRef {
1068        let exc = self
1069            .new_payload_exception::<PyImportError>(
1070                self.ctx.exceptions.import_error.to_owned(),
1071                vec![self.ctx.new_str(msg.into()).into()].into(),
1072            )
1073            .expect("ImportError construction from internal args is infallible");
1074        set_attrs!(
1075            exc.as_object(), self, unwrap,
1076            "name" => name.into(),
1077        );
1078
1079        exc.upcast()
1080    }
1081
1082    pub fn new_system_exit(&self, args: FuncArgs) -> PyBaseExceptionRef {
1083        self.new_payload_exception::<PySystemExit>(self.ctx.exceptions.system_exit.to_owned(), args)
1084            .expect("SystemExit construction from internal args is infallible")
1085            .upcast()
1086    }
1087
1088    pub fn new_stop_iteration(&self, value: Option<PyObjectRef>) -> PyBaseExceptionRef {
1089        // None (or omitted) is PyErr_SetNone: args stay empty so
1090        // format_exception_only prints "StopIteration" not "StopIteration: None".
1091        let args: FuncArgs = match value {
1092            Some(v) if !self.is_none(&v) => vec![v].into(),
1093            _ => Vec::<PyObjectRef>::new().into(),
1094        };
1095        self.new_payload_exception::<PyStopIteration>(
1096            self.ctx.exceptions.stop_iteration.to_owned(),
1097            args,
1098        )
1099        .expect("StopIteration construction from internal args is infallible")
1100        .upcast()
1101    }
1102
1103    fn new_downcast_error(
1104        &self,
1105        msg: &'static str,
1106        error_type: &'static Py<PyType>,
1107        class: &Py<PyType>,
1108        obj: &PyObject, // the impl Borrow allows to pass PyObjectRef or &PyObject
1109    ) -> PyBaseExceptionRef {
1110        let actual_class = obj.class();
1111        let actual_type = &*actual_class.name();
1112        let expected_type = &*class.name();
1113        let msg = format!("Expected {msg} '{expected_type}' but '{actual_type}' found.");
1114        #[cfg(debug_assertions)]
1115        let msg = if class.get_id() == actual_class.get_id() {
1116            let mut msg = msg;
1117            msg += " It might mean this type doesn't support subclassing very well. e.g. Did you forget to add `#[pyclass(with(Constructor))]`?";
1118            msg
1119        } else {
1120            msg
1121        };
1122        self.new_exception_msg(error_type.to_owned(), msg.into())
1123    }
1124
1125    pub(crate) fn new_downcast_runtime_error(
1126        &self,
1127        class: &Py<PyType>,
1128        obj: &impl AsObject,
1129    ) -> PyBaseExceptionRef {
1130        self.new_downcast_error(
1131            "payload",
1132            self.ctx.exceptions.runtime_error,
1133            class,
1134            obj.as_object(),
1135        )
1136    }
1137
1138    pub(crate) fn new_downcast_type_error(
1139        &self,
1140        class: &Py<PyType>,
1141        obj: &impl AsObject,
1142    ) -> PyBaseExceptionRef {
1143        self.new_downcast_error(
1144            "type",
1145            self.ctx.exceptions.type_error,
1146            class,
1147            obj.as_object(),
1148        )
1149    }
1150
1151    /// Create a `MemoryError` with no arguments, for reporting a failed allocation.
1152    ///
1153    /// Served from the MemoryError freelist when a husk is available, so the
1154    /// raise itself does not allocate; falls back to a fresh allocation when
1155    /// the freelist is empty (#8536).
1156    pub fn no_memory_error(&self) -> PyBaseExceptionRef {
1157        let exc: PyObjectRef = PyMemoryError::empty(self)
1158            .into_ref_with_type_lazy_dict(self, self.ctx.exceptions.memory_error.to_owned())
1159            .expect("MemoryError is a static type whose payload size matches PyMemoryError")
1160            .into();
1161        exc.downcast()
1162            .expect("PyMemoryError payload downcasts to PyBaseException")
1163    }
1164
1165    define_exception_fn!(fn new_lookup_error, lookup_error, LookupError);
1166    define_exception_fn!(fn new_eof_error, eof_error, EOFError);
1167    pub fn new_attribute_error(&self, msg: impl Into<Wtf8Buf>) -> PyBaseExceptionRef {
1168        self.new_payload_exception::<PyAttributeError>(
1169            self.ctx.exceptions.attribute_error.to_owned(),
1170            vec![self.ctx.new_str(msg.into()).into()].into(),
1171        )
1172        .expect("AttributeError construction from internal args is infallible")
1173        .upcast()
1174    }
1175
1176    define_exception_fn!(fn new_type_error, type_error, TypeError);
1177    define_exception_fn!(fn new_system_error, system_error, SystemError);
1178
1179    define_exception_fn!(fn new_value_error, value_error, ValueError);
1180
1181    define_exception_fn!(fn new_buffer_error, buffer_error, BufferError);
1182    define_exception_fn!(fn new_index_error, index_error, IndexError);
1183    define_exception_fn!(
1184     fn new_not_implemented_error,
1185        not_implemented_error,
1186        NotImplementedError
1187    );
1188    define_exception_fn!(fn new_recursion_error, recursion_error, RecursionError);
1189    define_exception_fn!(fn new_zero_division_error, zero_division_error, ZeroDivisionError);
1190    define_exception_fn!(fn new_overflow_error, overflow_error, OverflowError);
1191    define_exception_fn!(fn new_runtime_error, runtime_error, RuntimeError);
1192    define_exception_fn!(fn new_python_finalization_error, python_finalization_error, PythonFinalizationError);
1193    define_exception_fn!(fn new_memory_error, memory_error, MemoryError);
1194    define_exception_fn!(fn new_assertion_error, assertion_error, AssertionError);
1195    pub fn new_unbound_local_error(&self, msg: impl Into<Wtf8Buf>) -> PyBaseExceptionRef {
1196        self.new_payload_exception::<PyNameError>(
1197            self.ctx.exceptions.unbound_local_error.to_owned(),
1198            vec![self.ctx.new_str(msg.into()).into()].into(),
1199        )
1200        .expect("UnboundLocalError construction from internal args is infallible")
1201        .upcast()
1202    }
1203}
1204
1205#[cfg(feature = "parser")]
1206enum QuotedStringScan {
1207    Closed(usize),
1208    Unclosed {
1209        triple: bool,
1210        unescaped_newline: bool,
1211    },
1212}
1213
1214/// An unclosed string is incomplete when more input could still finish it.
1215/// A non-triple string that already hit an unescaped newline is a hard error.
1216#[cfg(feature = "parser")]
1217fn unclosed_string_is_incomplete(source: &str) -> bool {
1218    let bytes = source.as_bytes();
1219    let mut index = 0;
1220    while index < bytes.len() {
1221        match bytes[index] {
1222            b'#' => {
1223                while index < bytes.len() && bytes[index] != b'\n' {
1224                    index += 1;
1225                }
1226            }
1227            b'\'' | b'"' => match scan_quoted_string_for_incomplete(bytes, index) {
1228                QuotedStringScan::Closed(end) => index = end,
1229                QuotedStringScan::Unclosed {
1230                    triple,
1231                    unescaped_newline,
1232                } => {
1233                    // Single-quoted f/t-strings never set E_EOLS.
1234                    if !triple && interpolated_string_prefix_at(bytes, index) {
1235                        return false;
1236                    }
1237                    return triple || !unescaped_newline;
1238                }
1239            },
1240            _ => index += 1,
1241        }
1242    }
1243    false
1244}
1245
1246#[cfg(feature = "parser")]
1247fn interpolated_string_prefix_at(bytes: &[u8], quote: usize) -> bool {
1248    let Some(&prev) = quote.checked_sub(1).and_then(|index| bytes.get(index)) else {
1249        return false;
1250    };
1251    let lower = prev.to_ascii_lowercase();
1252    let marker_index = if matches!(lower, b'f' | b't') {
1253        if quote >= 2 && bytes[quote - 2].eq_ignore_ascii_case(&b'r') {
1254            quote - 2
1255        } else {
1256            quote - 1
1257        }
1258    } else if lower == b'r'
1259        && quote >= 2
1260        && matches!(bytes[quote - 2].to_ascii_lowercase(), b'f' | b't')
1261    {
1262        quote - 2
1263    } else {
1264        return false;
1265    };
1266    marker_index == 0 || !identifier_continue_before(bytes, marker_index)
1267}
1268
1269#[cfg(feature = "parser")]
1270fn identifier_continue_before(bytes: &[u8], index: usize) -> bool {
1271    if index == 0 {
1272        return false;
1273    }
1274    if bytes[index - 1].is_ascii() {
1275        return bytes[index - 1] == b'_' || bytes[index - 1].is_ascii_alphanumeric();
1276    }
1277    let mut start = index - 1;
1278    while start > 0 && bytes[start] & 0b1100_0000 == 0b1000_0000 {
1279        start -= 1;
1280    }
1281    ::core::str::from_utf8(&bytes[start..index])
1282        .ok()
1283        .and_then(|text| text.chars().next_back())
1284        .is_some_and(|ch| ch == '_' || ch.is_alphanumeric())
1285}
1286
1287#[cfg(feature = "parser")]
1288fn scan_quoted_string_for_incomplete(bytes: &[u8], quote_index: usize) -> QuotedStringScan {
1289    let quote = bytes[quote_index];
1290    let triple =
1291        bytes.get(quote_index + 1) == Some(&quote) && bytes.get(quote_index + 2) == Some(&quote);
1292    let mut index = quote_index + if triple { 3 } else { 1 };
1293    while index < bytes.len() {
1294        if bytes[index] == b'\\' {
1295            index = (index + 2).min(bytes.len());
1296            continue;
1297        }
1298        if triple {
1299            if bytes.get(index) == Some(&quote)
1300                && bytes.get(index + 1) == Some(&quote)
1301                && bytes.get(index + 2) == Some(&quote)
1302            {
1303                return QuotedStringScan::Closed(index + 3);
1304            }
1305        } else if bytes[index] == quote {
1306            return QuotedStringScan::Closed(index + 1);
1307        } else if bytes[index] == b'\n' {
1308            return QuotedStringScan::Unclosed {
1309                triple: false,
1310                unescaped_newline: true,
1311            };
1312        }
1313        index += 1;
1314    }
1315    QuotedStringScan::Unclosed {
1316        triple,
1317        unescaped_newline: false,
1318    }
1319}