Merge branch 'main' into hy/close_issue_137942

This commit is contained in:
yihong 2025-08-22 11:38:40 +08:00 committed by GitHub
commit a2d5480a46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 2445 additions and 542 deletions

View file

@ -971,10 +971,17 @@ as globals. It would be impossible to assign to a global variable without
:keyword:`!global`, although free variables may refer to globals without being
declared global.
The :keyword:`global` statement applies to the entire scope of a function or
class body. A :exc:`SyntaxError` is raised if a variable is used or
The :keyword:`!global` statement applies to the entire current scope
(module, function body or class definition).
A :exc:`SyntaxError` is raised if a variable is used or
assigned to prior to its global declaration in the scope.
At the module level, all variables are global, so a :keyword:`!global`
statement has no effect.
However, variables must still not be used or
assigned to prior to their :keyword:`!global` declaration.
This requirement is relaxed in the interactive prompt (:term:`REPL`).
.. index::
pair: built-in function; exec
pair: built-in function; eval

View file

@ -359,7 +359,7 @@ Usually, a method is called right after it is bound::
x.f()
In the :class:`!MyClass` example, this will return the string ``'hello world'``.
If ``x = MyClass()``, as above, this will return the string ``'hello world'``.
However, it is not necessary to call a method right away: ``x.f`` is a method
object, and can be stored away and called at a later time. For example::

View file

@ -169,6 +169,45 @@ production systems where traditional profiling approaches would be too intrusive
(Contributed by Pablo Galindo and László Kiss Kollár in :gh:`135953`.)
Improved error messages
-----------------------
* The interpreter now provides more helpful suggestions in :exc:`AttributeError`
exceptions when accessing an attribute on an object that does not exist, but
a similar attribute is available through one of its members.
For example, if the object has an attribute that itself exposes the requested
name, the error message will suggest accessing it via that inner attribute:
.. code-block:: python
@dataclass
class Circle:
radius: float
@property
def area(self) -> float:
return pi * self.radius**2
class Container:
def __init__(self, inner: Circle) -> None:
self.inner = inner
circle = Circle(radius=4.0)
container = Container(circle)
print(container.area)
Running this code now produces a clearer suggestion:
.. code-block:: pycon
Traceback (most recent call last):
File "/home/pablogsal/github/python/main/lel.py", line 42, in <module>
print(container.area)
^^^^^^^^^^^^^^
AttributeError: 'Container' object has no attribute 'area'. Did you mean: 'inner.area'?
Other language changes
======================

View file

@ -123,6 +123,22 @@ _PyEval_EvalFrame(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwfl
return tstate->interp->eval_frame(tstate, frame, throwflag);
}
#ifdef _Py_TIER2
#ifdef _Py_JIT
_Py_CODEUNIT *_Py_LazyJitTrampoline(
struct _PyExecutorObject *current_executor, _PyInterpreterFrame *frame,
_PyStackRef *stack_pointer, PyThreadState *tstate
);
#else
_Py_CODEUNIT *_PyTier2Interpreter(
struct _PyExecutorObject *current_executor, _PyInterpreterFrame *frame,
_PyStackRef *stack_pointer, PyThreadState *tstate
);
#endif
#endif
extern _PyJitEntryFuncPtr _Py_jit_entry;
extern PyObject*
_PyEval_Vector(PyThreadState *tstate,
PyFunctionObject *func, PyObject *locals,

View file

@ -765,6 +765,7 @@ struct _Py_unique_id_pool {
#endif
typedef _Py_CODEUNIT *(*_PyJitEntryFuncPtr)(struct _PyExecutorObject *exec, _PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *tstate);
/* PyInterpreterState holds the global state for one of the runtime's
interpreters. Typically the initial (main) interpreter is the only one.

View file

@ -82,7 +82,6 @@ typedef struct _PyExecutorObject {
uint32_t code_size;
size_t jit_size;
void *jit_code;
void *jit_side_entry;
_PyExitData exits[1];
} _PyExecutorObject;

View file

@ -4262,6 +4262,184 @@ def __getattribute__(self, attr):
self.assertIn("Did you mean", actual)
self.assertIn("bluch", actual)
def test_getattr_nested_attribute_suggestions(self):
# Test that nested attributes are suggested when no direct match
class Inner:
def __init__(self):
self.value = 42
self.data = "test"
class Outer:
def __init__(self):
self.inner = Inner()
# Should suggest 'inner.value'
actual = self.get_suggestion(Outer(), 'value')
self.assertIn("Did you mean: 'inner.value'", actual)
# Should suggest 'inner.data'
actual = self.get_suggestion(Outer(), 'data')
self.assertIn("Did you mean: 'inner.data'", actual)
def test_getattr_nested_prioritizes_direct_matches(self):
# Test that direct attribute matches are prioritized over nested ones
class Inner:
def __init__(self):
self.foo = 42
class Outer:
def __init__(self):
self.inner = Inner()
self.fooo = 100 # Similar to 'foo'
# Should suggest 'fooo' (direct) not 'inner.foo' (nested)
actual = self.get_suggestion(Outer(), 'foo')
self.assertIn("Did you mean: 'fooo'", actual)
self.assertNotIn("inner.foo", actual)
def test_getattr_nested_with_property(self):
# Test that descriptors (including properties) are suggested in nested attributes
class Inner:
@property
def computed(self):
return 42
class Outer:
def __init__(self):
self.inner = Inner()
actual = self.get_suggestion(Outer(), 'computed')
# Descriptors should not be suggested to avoid executing arbitrary code
self.assertIn("inner.computed", actual)
def test_getattr_nested_no_suggestion_for_deep_nesting(self):
# Test that deeply nested attributes (2+ levels) are not suggested
class Deep:
def __init__(self):
self.value = 42
class Middle:
def __init__(self):
self.deep = Deep()
class Outer:
def __init__(self):
self.middle = Middle()
# Should not suggest 'middle.deep.value' (too deep)
actual = self.get_suggestion(Outer(), 'value')
self.assertNotIn("Did you mean", actual)
def test_getattr_nested_ignores_private_attributes(self):
# Test that nested suggestions ignore private attributes
class Inner:
def __init__(self):
self.public_value = 42
class Outer:
def __init__(self):
self._private_inner = Inner()
# Should not suggest '_private_inner.public_value'
actual = self.get_suggestion(Outer(), 'public_value')
self.assertNotIn("Did you mean", actual)
def test_getattr_nested_limits_attribute_checks(self):
# Test that nested suggestions are limited to checking first 20 non-private attributes
class Inner:
def __init__(self):
self.target_value = 42
class Outer:
def __init__(self):
# Add many attributes before 'inner'
for i in range(25):
setattr(self, f'attr_{i:02d}', i)
# Add the inner object after 20+ attributes
self.inner = Inner()
obj = Outer()
# Verify that 'inner' is indeed present but after position 20
attrs = [x for x in sorted(dir(obj)) if not x.startswith('_')]
inner_position = attrs.index('inner')
self.assertGreater(inner_position, 19, "inner should be after position 20 in sorted attributes")
# Should not suggest 'inner.target_value' because inner is beyond the first 20 attributes checked
actual = self.get_suggestion(obj, 'target_value')
self.assertNotIn("inner.target_value", actual)
def test_getattr_nested_returns_first_match_only(self):
# Test that only the first nested match is returned (not multiple)
class Inner1:
def __init__(self):
self.value = 1
class Inner2:
def __init__(self):
self.value = 2
class Inner3:
def __init__(self):
self.value = 3
class Outer:
def __init__(self):
# Multiple inner objects with same attribute
self.a_inner = Inner1()
self.b_inner = Inner2()
self.c_inner = Inner3()
# Should suggest only the first match (alphabetically)
actual = self.get_suggestion(Outer(), 'value')
self.assertIn("'a_inner.value'", actual)
# Verify it's a single suggestion, not multiple
self.assertEqual(actual.count("Did you mean"), 1)
def test_getattr_nested_handles_attribute_access_exceptions(self):
# Test that exceptions raised when accessing attributes don't crash the suggestion system
class ExplodingProperty:
@property
def exploding_attr(self):
raise RuntimeError("BOOM! This property always explodes")
def __repr__(self):
raise RuntimeError("repr also explodes")
class SafeInner:
def __init__(self):
self.target = 42
class Outer:
def __init__(self):
self.exploder = ExplodingProperty() # Accessing attributes will raise
self.safe_inner = SafeInner()
# Should still suggest 'safe_inner.target' without crashing
# even though accessing exploder.target would raise an exception
actual = self.get_suggestion(Outer(), 'target')
self.assertIn("'safe_inner.target'", actual)
def test_getattr_nested_handles_hasattr_exceptions(self):
# Test that exceptions in hasattr don't crash the system
class WeirdObject:
def __getattr__(self, name):
if name == 'target':
raise RuntimeError("Can't check for target attribute")
raise AttributeError(f"No attribute {name}")
class NormalInner:
def __init__(self):
self.target = 100
class Outer:
def __init__(self):
self.weird = WeirdObject() # hasattr will raise for 'target'
self.normal = NormalInner()
# Should still find 'normal.target' even though weird.target check fails
actual = self.get_suggestion(Outer(), 'target')
self.assertIn("'normal.target'", actual)
def make_module(self, code):
tmpdir = Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, tmpdir)

View file

@ -1601,6 +1601,34 @@ def _substitution_cost(ch_a, ch_b):
return _MOVE_COST
def _check_for_nested_attribute(obj, wrong_name, attrs):
"""Check if any attribute of obj has the wrong_name as a nested attribute.
Returns the first nested attribute suggestion found, or None.
Limited to checking 20 attributes.
Only considers non-descriptor attributes to avoid executing arbitrary code.
"""
# Check for nested attributes (only one level deep)
attrs_to_check = [x for x in attrs if not x.startswith('_')][:20] # Limit number of attributes to check
for attr_name in attrs_to_check:
with suppress(Exception):
# Check if attr_name is a descriptor - if so, skip it
attr_from_class = getattr(type(obj), attr_name, None)
if attr_from_class is not None and hasattr(attr_from_class, '__get__'):
continue # Skip descriptors to avoid executing arbitrary code
# Safe to get the attribute since it's not a descriptor
attr_obj = getattr(obj, attr_name)
# Check if the nested attribute exists and is not a descriptor
nested_attr_from_class = getattr(type(attr_obj), wrong_name, None)
if hasattr(attr_obj, wrong_name):
return f"{attr_name}.{wrong_name}"
return None
def _compute_suggestion_error(exc_value, tb, wrong_name):
if wrong_name is None or not isinstance(wrong_name, str):
return None
@ -1666,7 +1694,9 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
except ImportError:
pass
else:
return _suggestions._generate_suggestions(d, wrong_name)
suggestion = _suggestions._generate_suggestions(d, wrong_name)
if suggestion:
return suggestion
# Compute closest match
@ -1691,6 +1721,14 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
if not suggestion or current_distance < best_distance:
suggestion = possible_name
best_distance = current_distance
# If no direct attribute match found, check for nested attributes
if not suggestion and isinstance(exc_value, AttributeError):
with suppress(Exception):
nested_suggestion = _check_for_nested_attribute(exc_value.obj, wrong_name, d)
if nested_suggestion:
return nested_suggestion
return suggestion

View file

@ -0,0 +1,2 @@
Replace the shim code added to every piece of jitted code with a single
trampoline function.

View file

@ -0,0 +1 @@
Show error suggestions on nested attribute access. Patch by Pablo Galindo

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -10,53 +10,6 @@
#include "pymacro.h"
PyDoc_STRVAR(doc_is_canonical,
"is_canonical($self, /)\n--\n\n\
Return True if the argument is canonical and False otherwise. Currently,\n\
a Decimal instance is always canonical, so this operation always returns\n\
True.\n\
\n");
PyDoc_STRVAR(doc_is_finite,
"is_finite($self, /)\n--\n\n\
Return True if the argument is a finite number, and False if the argument\n\
is infinite or a NaN.\n\
\n");
PyDoc_STRVAR(doc_is_infinite,
"is_infinite($self, /)\n--\n\n\
Return True if the argument is either positive or negative infinity and\n\
False otherwise.\n\
\n");
PyDoc_STRVAR(doc_is_nan,
"is_nan($self, /)\n--\n\n\
Return True if the argument is a (quiet or signaling) NaN and False\n\
otherwise.\n\
\n");
PyDoc_STRVAR(doc_is_qnan,
"is_qnan($self, /)\n--\n\n\
Return True if the argument is a quiet NaN, and False otherwise.\n\
\n");
PyDoc_STRVAR(doc_is_signed,
"is_signed($self, /)\n--\n\n\
Return True if the argument has a negative sign and False otherwise.\n\
Note that both zeros and NaNs can carry signs.\n\
\n");
PyDoc_STRVAR(doc_is_snan,
"is_snan($self, /)\n--\n\n\
Return True if the argument is a signaling NaN and False otherwise.\n\
\n");
PyDoc_STRVAR(doc_is_zero,
"is_zero($self, /)\n--\n\n\
Return True if the argument is a (positive or negative) zero and False\n\
otherwise.\n\
\n");
/******************************************************************************/
/* Context Object and Methods */
/******************************************************************************/
@ -114,41 +67,11 @@ Create a new Decimal instance from float f. Unlike the Decimal.from_float()\n\
class method, this function observes the context limits.\n\
\n");
PyDoc_STRVAR(doc_ctx_abs,
"abs($self, x, /)\n--\n\n\
Return the absolute value of x.\n\
\n");
PyDoc_STRVAR(doc_ctx_add,
"add($self, x, y, /)\n--\n\n\
Return the sum of x and y.\n\
\n");
PyDoc_STRVAR(doc_ctx_canonical,
"canonical($self, x, /)\n--\n\n\
Return a new instance of x.\n\
\n");
PyDoc_STRVAR(doc_ctx_compare,
"compare($self, x, y, /)\n--\n\n\
Compare x and y numerically.\n\
\n");
PyDoc_STRVAR(doc_ctx_compare_signal,
"compare_signal($self, x, y, /)\n--\n\n\
Compare x and y numerically. All NaNs signal.\n\
\n");
PyDoc_STRVAR(doc_ctx_compare_total,
"compare_total($self, x, y, /)\n--\n\n\
Compare x and y using their abstract representation.\n\
\n");
PyDoc_STRVAR(doc_ctx_compare_total_mag,
"compare_total_mag($self, x, y, /)\n--\n\n\
Compare x and y using their abstract representation, ignoring sign.\n\
\n");
PyDoc_STRVAR(doc_ctx_copy_abs,
"copy_abs($self, x, /)\n--\n\n\
Return a copy of x with the sign set to 0.\n\
@ -164,196 +87,16 @@ PyDoc_STRVAR(doc_ctx_copy_sign,
Copy the sign from y to x.\n\
\n");
PyDoc_STRVAR(doc_ctx_divide,
"divide($self, x, y, /)\n--\n\n\
Return x divided by y.\n\
\n");
PyDoc_STRVAR(doc_ctx_divide_int,
"divide_int($self, x, y, /)\n--\n\n\
Return x divided by y, truncated to an integer.\n\
\n");
PyDoc_STRVAR(doc_ctx_divmod,
"divmod($self, x, y, /)\n--\n\n\
Return quotient and remainder of the division x / y.\n\
\n");
PyDoc_STRVAR(doc_ctx_exp,
"exp($self, x, /)\n--\n\n\
Return e ** x.\n\
\n");
PyDoc_STRVAR(doc_ctx_fma,
"fma($self, x, y, z, /)\n--\n\n\
Return x multiplied by y, plus z.\n\
\n");
PyDoc_STRVAR(doc_compare_total,
"compare_total($self, /, other, context=None)\n--\n\n\
Compare two operands using their abstract representation rather than\n\
their numerical value. Similar to the compare() method, but the result\n\
gives a total ordering on Decimal instances. Two Decimal instances with\n\
the same numeric value but different representations compare unequal\n\
in this ordering:\n\
\n\
>>> Decimal('12.0').compare_total(Decimal('12'))\n\
Decimal('-1')\n\
\n\
Quiet and signaling NaNs are also included in the total ordering. The result\n\
of this function is Decimal('0') if both operands have the same representation,\n\
Decimal('-1') if the first operand is lower in the total order than the second,\n\
and Decimal('1') if the first operand is higher in the total order than the\n\
second operand. See the specification for details of the total order.\n\
\n\
This operation is unaffected by context and is quiet: no flags are changed\n\
and no rounding is performed. As an exception, the C version may raise\n\
InvalidOperation if the second operand cannot be converted exactly.\n\
\n");
PyDoc_STRVAR(doc_compare_total_mag,
"compare_total_mag($self, /, other, context=None)\n--\n\n\
Compare two operands using their abstract representation rather than their\n\
value as in compare_total(), but ignoring the sign of each operand.\n\
\n\
x.compare_total_mag(y) is equivalent to x.copy_abs().compare_total(y.copy_abs()).\n\
\n\
This operation is unaffected by context and is quiet: no flags are changed\n\
and no rounding is performed. As an exception, the C version may raise\n\
InvalidOperation if the second operand cannot be converted exactly.\n\
\n");
PyDoc_STRVAR(doc_ctx_is_canonical,
"is_canonical($self, x, /)\n--\n\n\
Return True if x is canonical, False otherwise.\n\
\n");
PyDoc_STRVAR(doc_ctx_is_finite,
"is_finite($self, x, /)\n--\n\n\
Return True if x is finite, False otherwise.\n\
\n");
PyDoc_STRVAR(doc_ctx_is_infinite,
"is_infinite($self, x, /)\n--\n\n\
Return True if x is infinite, False otherwise.\n\
\n");
PyDoc_STRVAR(doc_ctx_is_nan,
"is_nan($self, x, /)\n--\n\n\
Return True if x is a qNaN or sNaN, False otherwise.\n\
\n");
PyDoc_STRVAR(doc_ctx_is_normal,
"is_normal($self, x, /)\n--\n\n\
Return True if x is a normal number, False otherwise.\n\
\n");
PyDoc_STRVAR(doc_ctx_is_qnan,
"is_qnan($self, x, /)\n--\n\n\
Return True if x is a quiet NaN, False otherwise.\n\
\n");
PyDoc_STRVAR(doc_ctx_is_signed,
"is_signed($self, x, /)\n--\n\n\
Return True if x is negative, False otherwise.\n\
\n");
PyDoc_STRVAR(doc_ctx_is_snan,
"is_snan($self, x, /)\n--\n\n\
Return True if x is a signaling NaN, False otherwise.\n\
\n");
PyDoc_STRVAR(doc_ctx_is_subnormal,
"is_subnormal($self, x, /)\n--\n\n\
Return True if x is subnormal, False otherwise.\n\
\n");
PyDoc_STRVAR(doc_ctx_is_zero,
"is_zero($self, x, /)\n--\n\n\
Return True if x is a zero, False otherwise.\n\
\n");
PyDoc_STRVAR(doc_ctx_ln,
"ln($self, x, /)\n--\n\n\
Return the natural (base e) logarithm of x.\n\
\n");
PyDoc_STRVAR(doc_ctx_log10,
"log10($self, x, /)\n--\n\n\
Return the base 10 logarithm of x.\n\
\n");
PyDoc_STRVAR(doc_ctx_logb,
"logb($self, x, /)\n--\n\n\
Return the exponent of the magnitude of the operand's MSD.\n\
\n");
PyDoc_STRVAR(doc_ctx_logical_and,
"logical_and($self, x, y, /)\n--\n\n\
Digit-wise and of x and y.\n\
\n");
PyDoc_STRVAR(doc_ctx_logical_invert,
"logical_invert($self, x, /)\n--\n\n\
Invert all digits of x.\n\
\n");
PyDoc_STRVAR(doc_ctx_logical_or,
"logical_or($self, x, y, /)\n--\n\n\
Digit-wise or of x and y.\n\
\n");
PyDoc_STRVAR(doc_ctx_logical_xor,
"logical_xor($self, x, y, /)\n--\n\n\
Digit-wise xor of x and y.\n\
\n");
PyDoc_STRVAR(doc_ctx_max,
"max($self, x, y, /)\n--\n\n\
Compare the values numerically and return the maximum.\n\
\n");
PyDoc_STRVAR(doc_ctx_max_mag,
"max_mag($self, x, y, /)\n--\n\n\
Compare the values numerically with their sign ignored.\n\
\n");
PyDoc_STRVAR(doc_ctx_min,
"min($self, x, y, /)\n--\n\n\
Compare the values numerically and return the minimum.\n\
\n");
PyDoc_STRVAR(doc_ctx_min_mag,
"min_mag($self, x, y, /)\n--\n\n\
Compare the values numerically with their sign ignored.\n\
\n");
PyDoc_STRVAR(doc_ctx_minus,
"minus($self, x, /)\n--\n\n\
Minus corresponds to the unary prefix minus operator in Python, but applies\n\
the context to the result.\n\
\n");
PyDoc_STRVAR(doc_ctx_multiply,
"multiply($self, x, y, /)\n--\n\n\
Return the product of x and y.\n\
\n");
PyDoc_STRVAR(doc_ctx_next_minus,
"next_minus($self, x, /)\n--\n\n\
Return the largest representable number smaller than x.\n\
\n");
PyDoc_STRVAR(doc_ctx_next_plus,
"next_plus($self, x, /)\n--\n\n\
Return the smallest representable number larger than x.\n\
\n");
PyDoc_STRVAR(doc_ctx_next_toward,
"next_toward($self, x, y, /)\n--\n\n\
Return the number closest to x, in the direction towards y.\n\
\n");
PyDoc_STRVAR(doc_ctx_normalize,
"normalize($self, x, /)\n--\n\n\
Reduce x to its simplest form. Alias for reduce(x).\n\
@ -364,79 +107,16 @@ PyDoc_STRVAR(doc_ctx_number_class,
Return an indication of the class of x.\n\
\n");
PyDoc_STRVAR(doc_ctx_plus,
"plus($self, x, /)\n--\n\n\
Plus corresponds to the unary prefix plus operator in Python, but applies\n\
the context to the result.\n\
\n");
PyDoc_STRVAR(doc_ctx_quantize,
"quantize($self, x, y, /)\n--\n\n\
Return a value equal to x (rounded), having the exponent of y.\n\
\n");
PyDoc_STRVAR(doc_ctx_remainder,
"remainder($self, x, y, /)\n--\n\n\
Return the remainder from integer division. The sign of the result,\n\
if non-zero, is the same as that of the original dividend.\n\
\n");
PyDoc_STRVAR(doc_ctx_remainder_near,
"remainder_near($self, x, y, /)\n--\n\n\
Return x - y * n, where n is the integer nearest the exact value of x / y\n\
(if the result is 0 then its sign will be the sign of x).\n\
\n");
PyDoc_STRVAR(doc_ctx_rotate,
"rotate($self, x, y, /)\n--\n\n\
Return a copy of x, rotated by y places.\n\
\n");
PyDoc_STRVAR(doc_ctx_same_quantum,
"same_quantum($self, x, y, /)\n--\n\n\
Return True if the two operands have the same exponent.\n\
\n");
PyDoc_STRVAR(doc_ctx_scaleb,
"scaleb($self, x, y, /)\n--\n\n\
Return the first operand after adding the second value to its exp.\n\
\n");
PyDoc_STRVAR(doc_ctx_shift,
"shift($self, x, y, /)\n--\n\n\
Return a copy of x, shifted by y places.\n\
\n");
PyDoc_STRVAR(doc_ctx_sqrt,
"sqrt($self, x, /)\n--\n\n\
Square root of a non-negative number to context precision.\n\
\n");
PyDoc_STRVAR(doc_ctx_subtract,
"subtract($self, x, y, /)\n--\n\n\
Return the difference between x and y.\n\
\n");
PyDoc_STRVAR(doc_ctx_to_eng_string,
"to_eng_string($self, x, /)\n--\n\n\
Convert a number to a string, using engineering notation.\n\
\n");
PyDoc_STRVAR(doc_ctx_to_integral,
"to_integral($self, x, /)\n--\n\n\
Identical to to_integral_value(x).\n\
\n");
PyDoc_STRVAR(doc_ctx_to_integral_exact,
"to_integral_exact($self, x, /)\n--\n\n\
Round to an integer. Signal if the result is rounded or inexact.\n\
\n");
PyDoc_STRVAR(doc_ctx_to_integral_value,
"to_integral_value($self, x, /)\n--\n\n\
Round to an integer.\n\
\n");
PyDoc_STRVAR(doc_ctx_to_sci_string,
"to_sci_string($self, x, /)\n--\n\n\
Convert a number to a string using scientific notation.\n\

View file

@ -1603,7 +1603,7 @@ queuesmod_put(PyObject *self, PyObject *args, PyObject *kwds)
PyObject *obj;
int unboundarg = -1;
int fallbackarg = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O|ii$p:put", kwlist,
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O|ii:put", kwlist,
qidarg_converter, &qidarg, &obj,
&unboundarg, &fallbackarg))
{

View file

@ -2971,7 +2971,7 @@ dummy_func(
assert(tstate->current_executor == NULL);
assert(executor != tstate->interp->cold_executor);
tstate->jit_exit = NULL;
GOTO_TIER_TWO(executor);
TIER1_TO_TIER2(executor);
}
}
else {
@ -3037,7 +3037,7 @@ dummy_func(
}
assert(executor != tstate->interp->cold_executor);
tstate->jit_exit = NULL;
GOTO_TIER_TWO(executor);
TIER1_TO_TIER2(executor);
#else
Py_FatalError("ENTER_EXECUTOR is not supported in this build");
#endif /* _Py_TIER2 */
@ -5257,7 +5257,7 @@ dummy_func(
}
#endif
tstate->jit_exit = exit;
GOTO_TIER_TWO(exit->executor);
TIER2_TO_TIER2(exit->executor);
}
tier2 op(_CHECK_VALIDITY, (--)) {
@ -5353,7 +5353,7 @@ dummy_func(
tier2 op(_START_EXECUTOR, (executor/4 --)) {
#ifndef _Py_JIT
current_executor = (_PyExecutorObject*)executor;
assert(current_executor == (_PyExecutorObject*)executor);
#endif
assert(tstate->jit_exit == NULL || tstate->jit_exit->executor == current_executor);
tstate->current_executor = (PyObject *)executor;
@ -5434,7 +5434,7 @@ dummy_func(
}
assert(tstate->jit_exit == exit);
exit->executor = executor;
GOTO_TIER_TWO(exit->executor);
TIER2_TO_TIER2(exit->executor);
}
label(pop_2_error) {

View file

@ -275,7 +275,8 @@ maybe_lltrace_resume_frame(_PyInterpreterFrame *frame, PyObject *globals)
}
int r = PyDict_Contains(globals, &_Py_ID(__lltrace__));
if (r < 0) {
return -1;
PyErr_Clear();
return 0;
}
int lltrace = r * 5; // Levels 1-4 only trace uops
if (!lltrace) {
@ -1109,11 +1110,6 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
#endif
}
#if defined(_Py_TIER2) && !defined(_Py_JIT)
/* Tier 2 interpreter state */
_PyExecutorObject *current_executor = NULL;
const _PyUOpInstruction *next_uop = NULL;
#endif
#if Py_TAIL_CALL_INTERP
# if Py_STATS
return _TAIL_CALL_start_frame(frame, NULL, tstate, NULL, 0, lastopcode);
@ -1126,14 +1122,41 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
#endif
early_exit:
assert(_PyErr_Occurred(tstate));
_Py_LeaveRecursiveCallPy(tstate);
assert(frame->owner != FRAME_OWNED_BY_INTERPRETER);
// GH-99729: We need to unlink the frame *before* clearing it:
_PyInterpreterFrame *dying = frame;
frame = tstate->current_frame = dying->previous;
_PyEval_FrameClearAndPop(tstate, dying);
frame->return_offset = 0;
assert(frame->owner == FRAME_OWNED_BY_INTERPRETER);
/* Restore previous frame and exit */
tstate->current_frame = frame->previous;
return NULL;
}
#ifdef _Py_TIER2
// Tier 2 is also here!
enter_tier_two:
#ifdef _Py_JIT
assert(0);
_PyJitEntryFuncPtr _Py_jit_entry = _Py_LazyJitTrampoline;
#else
_PyJitEntryFuncPtr _Py_jit_entry = _PyTier2Interpreter;
#endif
#endif
#if defined(_Py_TIER2) && !defined(_Py_JIT)
_Py_CODEUNIT *
_PyTier2Interpreter(
_PyExecutorObject *current_executor, _PyInterpreterFrame *frame,
_PyStackRef *stack_pointer, PyThreadState *tstate
) {
const _PyUOpInstruction *next_uop;
int oparg;
tier2_start:
next_uop = current_executor->trace;
assert(next_uop->opcode == _START_EXECUTOR || next_uop->opcode == _COLD_EXIT);
#undef LOAD_IP
#define LOAD_IP(UNUSED) (void)0
@ -1151,7 +1174,6 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
#undef ENABLE_SPECIALIZATION_FT
#define ENABLE_SPECIALIZATION_FT 0
; // dummy statement after a label, before a declaration
uint16_t uopcode;
#ifdef Py_STATS
int lastuop = 0;
@ -1225,24 +1247,9 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
next_uop = current_executor->trace + target;
goto tier2_dispatch;
#endif // _Py_JIT
}
#endif // _Py_TIER2
early_exit:
assert(_PyErr_Occurred(tstate));
_Py_LeaveRecursiveCallPy(tstate);
assert(frame->owner != FRAME_OWNED_BY_INTERPRETER);
// GH-99729: We need to unlink the frame *before* clearing it:
_PyInterpreterFrame *dying = frame;
frame = tstate->current_frame = dying->previous;
_PyEval_FrameClearAndPop(tstate, dying);
frame->return_offset = 0;
assert(frame->owner == FRAME_OWNED_BY_INTERPRETER);
/* Restore previous frame and exit */
tstate->current_frame = frame->previous;
return NULL;
}
#ifdef DO_NOT_OPTIMIZE_INTERP_LOOP
# pragma optimize("", on)

View file

@ -133,9 +133,6 @@ do { \
_PyFrame_SetStackPointer(frame, stack_pointer); \
int lltrace = maybe_lltrace_resume_frame(frame, GLOBALS()); \
stack_pointer = _PyFrame_GetStackPointer(frame); \
if (lltrace < 0) { \
JUMP_TO_LABEL(exit_unwind); \
} \
frame->lltrace = lltrace; \
} while (0)
#else
@ -354,16 +351,10 @@ _PyFrame_SetStackPointer(frame, stack_pointer)
/* Tier-switching macros. */
#ifdef _Py_JIT
#define GOTO_TIER_TWO(EXECUTOR) \
#define TIER1_TO_TIER2(EXECUTOR) \
do { \
OPT_STAT_INC(traces_executed); \
_PyExecutorObject *_executor = (EXECUTOR); \
jit_func jitted = _executor->jit_code; \
/* Keep the shim frame alive via the executor: */ \
Py_INCREF(_executor); \
next_instr = jitted(frame, stack_pointer, tstate); \
Py_DECREF(_executor); \
next_instr = _Py_jit_entry((EXECUTOR), frame, stack_pointer, tstate); \
frame = tstate->current_frame; \
stack_pointer = _PyFrame_GetStackPointer(frame); \
if (next_instr == NULL) { \
@ -372,31 +363,21 @@ do { \
} \
DISPATCH(); \
} while (0)
#else
#define GOTO_TIER_TWO(EXECUTOR) \
do { \
OPT_STAT_INC(traces_executed); \
_PyExecutorObject *_executor = (EXECUTOR); \
next_uop = _executor->trace; \
assert(next_uop->opcode == _START_EXECUTOR || next_uop->opcode == _COLD_EXIT); \
goto enter_tier_two; \
#define TIER2_TO_TIER2(EXECUTOR) \
do { \
OPT_STAT_INC(traces_executed); \
current_executor = (EXECUTOR); \
goto tier2_start; \
} while (0)
#endif
#define GOTO_TIER_ONE(TARGET) \
do \
{ \
tstate->current_executor = NULL; \
next_instr = (TARGET); \
OPT_HIST(trace_uop_execution_counter, trace_run_length_hist); \
_PyFrame_SetStackPointer(frame, stack_pointer); \
stack_pointer = _PyFrame_GetStackPointer(frame); \
if (next_instr == NULL) \
{ \
next_instr = frame->instr_ptr; \
goto error; \
} \
DISPATCH(); \
return TARGET; \
} while (0)
#define CURRENT_OPARG() (next_uop[-1].oparg)

View file

@ -7122,7 +7122,7 @@
}
#endif
tstate->jit_exit = exit;
GOTO_TIER_TWO(exit->executor);
TIER2_TO_TIER2(exit->executor);
break;
}
@ -7400,7 +7400,7 @@
case _START_EXECUTOR: {
PyObject *executor = (PyObject *)CURRENT_OPERAND0();
#ifndef _Py_JIT
current_executor = (_PyExecutorObject*)executor;
assert(current_executor == (_PyExecutorObject*)executor);
#endif
assert(tstate->jit_exit == NULL || tstate->jit_exit->executor == current_executor);
tstate->current_executor = (PyObject *)executor;
@ -7503,7 +7503,7 @@
}
assert(tstate->jit_exit == exit);
exit->executor = executor;
GOTO_TIER_TWO(exit->executor);
TIER2_TO_TIER2(exit->executor);
break;
}

View file

@ -5493,7 +5493,7 @@
}
assert(executor != tstate->interp->cold_executor);
tstate->jit_exit = NULL;
GOTO_TIER_TWO(executor);
TIER1_TO_TIER2(executor);
#else
Py_FatalError("ENTER_EXECUTOR is not supported in this build");
#endif /* _Py_TIER2 */
@ -7667,7 +7667,7 @@
assert(tstate->current_executor == NULL);
assert(executor != tstate->interp->cold_executor);
tstate->jit_exit = NULL;
GOTO_TIER_TWO(executor);
TIER1_TO_TIER2(executor);
}
}
else {

View file

@ -494,10 +494,6 @@ _PyJIT_Compile(_PyExecutorObject *executor, const _PyUOpInstruction trace[], siz
size_t code_size = 0;
size_t data_size = 0;
jit_state state = {0};
group = &shim;
code_size += group->code_size;
data_size += group->data_size;
combine_symbol_mask(group->trampoline_mask, state.trampolines.mask);
for (size_t i = 0; i < length; i++) {
const _PyUOpInstruction *instruction = &trace[i];
group = &stencil_groups[instruction->opcode];
@ -539,13 +535,6 @@ _PyJIT_Compile(_PyExecutorObject *executor, const _PyUOpInstruction trace[], siz
unsigned char *code = memory;
state.trampolines.mem = memory + code_size;
unsigned char *data = memory + code_size + state.trampolines.size + code_padding;
// Compile the shim, which handles converting between the native
// calling convention and the calling convention used by jitted code
// (which may be different for efficiency reasons).
group = &shim;
group->emit(code, data, executor, NULL, &state);
code += group->code_size;
data += group->data_size;
assert(trace[0].opcode == _START_EXECUTOR || trace[0].opcode == _COLD_EXIT);
for (size_t i = 0; i < length; i++) {
const _PyUOpInstruction *instruction = &trace[i];
@ -566,11 +555,75 @@ _PyJIT_Compile(_PyExecutorObject *executor, const _PyUOpInstruction trace[], siz
return -1;
}
executor->jit_code = memory;
executor->jit_side_entry = memory + shim.code_size;
executor->jit_size = total_size;
return 0;
}
/* One-off compilation of the jit entry trampoline
* We compile this once only as it effectively a normal
* function, but we need to use the JIT because it needs
* to understand the jit-specific calling convention.
*/
static _PyJitEntryFuncPtr
compile_trampoline(void)
{
_PyExecutorObject dummy;
const StencilGroup *group;
size_t code_size = 0;
size_t data_size = 0;
jit_state state = {0};
group = &trampoline;
code_size += group->code_size;
data_size += group->data_size;
combine_symbol_mask(group->trampoline_mask, state.trampolines.mask);
// Round up to the nearest page:
size_t page_size = get_page_size();
assert((page_size & (page_size - 1)) == 0);
size_t code_padding = DATA_ALIGN - ((code_size + state.trampolines.size) & (DATA_ALIGN - 1));
size_t padding = page_size - ((code_size + state.trampolines.size + code_padding + data_size) & (page_size - 1));
size_t total_size = code_size + state.trampolines.size + code_padding + data_size + padding;
unsigned char *memory = jit_alloc(total_size);
if (memory == NULL) {
return NULL;
}
unsigned char *code = memory;
state.trampolines.mem = memory + code_size;
unsigned char *data = memory + code_size + state.trampolines.size + code_padding;
// Compile the shim, which handles converting between the native
// calling convention and the calling convention used by jitted code
// (which may be different for efficiency reasons).
group = &trampoline;
group->emit(code, data, &dummy, NULL, &state);
code += group->code_size;
data += group->data_size;
assert(code == memory + code_size);
assert(data == memory + code_size + state.trampolines.size + code_padding + data_size);
if (mark_executable(memory, total_size)) {
jit_free(memory, total_size);
return NULL;
}
return (_PyJitEntryFuncPtr)memory;
}
static PyMutex lazy_jit_mutex = { 0 };
_Py_CODEUNIT *
_Py_LazyJitTrampoline(
_PyExecutorObject *executor, _PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *tstate
) {
PyMutex_Lock(&lazy_jit_mutex);
if (_Py_jit_entry == _Py_LazyJitTrampoline) {
_PyJitEntryFuncPtr trampoline = compile_trampoline();
if (trampoline == NULL) {
PyMutex_Unlock(&lazy_jit_mutex);
Py_FatalError("Cannot allocate core JIT code");
}
_Py_jit_entry = trampoline;
}
PyMutex_Unlock(&lazy_jit_mutex);
return _Py_jit_entry(executor, frame, stack_pointer, tstate);
}
void
_PyJIT_Free(_PyExecutorObject *executor)
{
@ -578,7 +631,6 @@ _PyJIT_Free(_PyExecutorObject *executor)
size_t size = executor->jit_size;
if (memory) {
executor->jit_code = NULL;
executor->jit_side_entry = NULL;
executor->jit_size = 0;
if (jit_free(memory, size)) {
PyErr_FormatUnraisable("Exception ignored while "

View file

@ -1238,7 +1238,6 @@ make_executor_from_uops(_PyUOpInstruction *buffer, int length, const _PyBloomFil
#endif
#ifdef _Py_JIT
executor->jit_code = NULL;
executor->jit_side_entry = NULL;
executor->jit_size = 0;
// This is initialized to true so we can prevent the executor
// from being immediately detected as cold and invalidated.
@ -1490,7 +1489,6 @@ _PyExecutor_GetColdExecutor(void)
((_PyUOpInstruction *)cold->trace)->opcode = _COLD_EXIT;
#ifdef _Py_JIT
cold->jit_code = NULL;
cold->jit_side_entry = NULL;
cold->jit_size = 0;
// This is initialized to true so we can prevent the executor
// from being immediately detected as cold and invalidated.

View file

@ -494,6 +494,11 @@ free_interpreter(PyInterpreterState *interp)
static inline int check_interpreter_whence(long);
#endif
extern _Py_CODEUNIT *
_Py_LazyJitTrampoline(
struct _PyExecutorObject *exec, _PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *tstate
);
/* Get the interpreter state to a minimal consistent state.
Further init happens in pylifecycle.c before it can be used.
All fields not initialized here are expected to be zeroed out,

View file

@ -191,8 +191,8 @@ async def _build_stencils(self) -> dict[str, _stencils.StencilGroup]:
with tempfile.TemporaryDirectory() as tempdir:
work = pathlib.Path(tempdir).resolve()
async with asyncio.TaskGroup() as group:
coro = self._compile("shim", TOOLS_JIT / "shim.c", work)
tasks.append(group.create_task(coro, name="shim"))
coro = self._compile("trampoline", TOOLS_JIT / "trampoline.c", work)
tasks.append(group.create_task(coro, name="trampoline"))
template = TOOLS_JIT_TEMPLATE_C.read_text()
for case, opname in cases_and_opnames:
# Write out a copy of the template with *only* this case

View file

@ -22,11 +22,11 @@ def _dump_footer(
yield " symbol_mask trampoline_mask;"
yield "} StencilGroup;"
yield ""
yield f"static const StencilGroup shim = {groups['shim'].as_c('shim')};"
yield f"static const StencilGroup trampoline = {groups['trampoline'].as_c('trampoline')};"
yield ""
yield "static const StencilGroup stencil_groups[MAX_UOP_ID + 1] = {"
for opname, group in sorted(groups.items()):
if opname == "shim":
if opname == "trampoline":
continue
yield f" [{opname}] = {group.as_c(opname)},"
yield "};"

View file

@ -1,15 +0,0 @@
#include "Python.h"
#include "pycore_ceval.h"
#include "pycore_frame.h"
#include "pycore_jit.h"
#include "jit.h"
_Py_CODEUNIT *
_JIT_ENTRY(_PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *tstate)
{
// Note that this is *not* a tail call:
DECLARE_TARGET(_JIT_CONTINUE);
return _JIT_CONTINUE(frame, stack_pointer, tstate);
}

View file

@ -46,12 +46,12 @@
#undef CURRENT_TARGET
#define CURRENT_TARGET() (_target)
#undef GOTO_TIER_TWO
#define GOTO_TIER_TWO(EXECUTOR) \
#undef TIER2_TO_TIER2
#define TIER2_TO_TIER2(EXECUTOR) \
do { \
OPT_STAT_INC(traces_executed); \
_PyExecutorObject *_executor = (EXECUTOR); \
jit_func_preserve_none jitted = _executor->jit_side_entry; \
jit_func_preserve_none jitted = _executor->jit_code; \
__attribute__((musttail)) return jitted(frame, stack_pointer, tstate); \
} while (0)

16
Tools/jit/trampoline.c Normal file
View file

@ -0,0 +1,16 @@
#include "Python.h"
#include "pycore_ceval.h"
#include "pycore_frame.h"
#include "pycore_jit.h"
#include "jit.h"
_Py_CODEUNIT *
_JIT_ENTRY(
_PyExecutorObject *exec, _PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *tstate
) {
typedef DECLARE_TARGET((*jit_func));
jit_func jitted = (jit_func)exec->jit_code;
return jitted(frame, stack_pointer, tstate);
}