From 6e3061a4bc7a2989e3cde383af1963b8e3e62555 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Tue, 5 May 2026 14:15:53 +0200 Subject: [PATCH 01/17] Move PyCallable_Check to C implementation Move the public PyCallable_Check symbol from the Java direct builtin to the existing C implementation in object.c and register it as CImpl in CApiFunction. Remove the Java PyCallable_Check builtin and unused PyCallableCheckNode import. Temporary benchmark coverage used for measurement: a c-pycallable-check microbenchmark covering a native callable with tp_call, a native non-callable object, and NULL-safe behavior. The temporary benchmark and benchmark registration were removed before committing per PLAN.org. Verification: - mx python-jvm passed. - JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest mx python-svm passed. - mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_object.py::tests.cpyext.test_object.TestObjectFunctions.test_PyCallable_Check passed: Ran 1 tests in 61.80s, OK (passed=1). - mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/ passed: Ran 2807 tests in 800.33s, OK (passed=2788, skipped=19). Benchmark command shape: --experimental-options --engine.Compilation=false graalpython/com.oracle.graal.python.benchmarks/python/harness.py -r 3 -i 9 graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pycallable-check.py 100000000 Benchmark quietness: observed the machine with vmstat 5 13 before both baseline and current measurements. The pre-baseline and pre-current windows were mostly r=0 with 99-100% idle after the initial sample, and no active build/test process was visible among top CPU consumers. Baseline measurement used a native standalone built with this implementation patch temporarily reversed and copied to /tmp/graalpy-pycallable-baseline-20260505-135844. Raw durations: [15.764891772, 15.176673579, 15.133897106, 15.679905815, 15.067935304, 14.92753357, 15.24439545, 14.782456798, 15.072720261]. Median: 15.133897106 s; best: 14.782456798 s; worst: 15.764891772 s; spread: 0.982434974 s. Current C implementation measurement raw durations: [0.295874113, 0.29998132, 0.30158966, 0.291079724, 0.288517571, 0.295749312, 0.29998344, 0.287563849, 0.287959175]. Median: 0.295749312 s; best: 0.287563849 s; worst: 0.30158966 s; spread: 0.014025811 s. Conclusion: moving PyCallable_Check from the Java direct builtin to the C implementation improves this native no-compilation benchmark by about 51.17x by median time. --- graalpython/com.oracle.graal.python.cext/src/object.c | 2 +- .../modules/cext/PythonCextObjectBuiltins.java | 11 ----------- .../builtins/objects/cext/capi/CApiFunction.java | 2 ++ 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/object.c b/graalpython/com.oracle.graal.python.cext/src/object.c index f8c6cadb2e..88d8ae5990 100644 --- a/graalpython/com.oracle.graal.python.cext/src/object.c +++ b/graalpython/com.oracle.graal.python.cext/src/object.c @@ -1819,7 +1819,6 @@ PyObject_Not(PyObject *v) return res == 0; } -#if 0 // GraalPy change /* Test whether an object can be called */ int @@ -1830,6 +1829,7 @@ PyCallable_Check(PyObject *x) return Py_TYPE(x)->tp_call != NULL; } +#if 0 // GraalPy change /* Helper for PyObject_Dir without arguments: returns the local scope. */ static PyObject * diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java index 3f393b3324..ad8c43c711 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java @@ -104,7 +104,6 @@ import com.oracle.graal.python.builtins.objects.tuple.PTuple; import com.oracle.graal.python.builtins.objects.type.TypeNodes; import com.oracle.graal.python.lib.PyBytesCheckNode; -import com.oracle.graal.python.lib.PyCallableCheckNode; import com.oracle.graal.python.lib.PyLongCheckNode; import com.oracle.graal.python.lib.PyObjectAsFileDescriptor; import com.oracle.graal.python.lib.PyObjectAsciiAsObjectNode; @@ -698,16 +697,6 @@ static long hash(Object object, } } - @CApiBuiltin(ret = Int, args = {PyObject}, call = Direct) - abstract static class PyCallable_Check extends CApiUnaryBuiltinNode { - @Specialization - static int doGeneric(Object object, - @Bind Node inliningTarget, - @Cached PyCallableCheckNode callableCheck) { - return intValue(callableCheck.execute(inliningTarget, object)); - } - } - @CApiBuiltin(ret = PyObjectTransfer, args = {PyObject}, call = Direct) abstract static class PyObject_Dir extends CApiUnaryBuiltinNode { @Specialization diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java index 2d124e3dca..7e1c8e6ea0 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java @@ -130,6 +130,7 @@ import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectConstPtr; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectPtr; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectRawPointer; +import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectReturn; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectTransfer; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PySendResult; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PySliceObject; @@ -234,6 +235,7 @@ public final class CApiFunction { @CApiBuiltin(name = "PyCFunction_New", ret = PyObject, args = {PyMethodDef, PyObject}, call = CImpl) @CApiBuiltin(name = "PyCFunction_NewEx", ret = PyObject, args = {PyMethodDef, PyObject, PyObject}, call = CImpl) @CApiBuiltin(name = "PyCMethod_New", ret = PyObject, args = {PyMethodDef, PyObject, PyObject, PyTypeObject}, call = CImpl) + @CApiBuiltin(name = "PyCallable_Check", ret = PrimitiveResult32, args = {PyObjectReturn}, call = CImpl) @CApiBuiltin(name = "PyUnstable_Code_New", ret = PyCodeObject, args = {Int, Int, Int, Int, Int, PyObject, PyObject, PyObject, PyObject, PyObject, PyObject, PyObject, PyObject, PyObject, Int, PyObject, PyObject}, call = CImpl) @CApiBuiltin(name = "PyCodec_StrictErrors", ret = PyObject, args = {PyObject}, call = CImpl) From f2989004159a41b15544d4fd944b78ee7bffaeab Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Tue, 5 May 2026 15:05:55 +0200 Subject: [PATCH 02/17] Move PyObject_IsTrue to C implementation Move the public PyObject_IsTrue C API symbol from the Java direct builtin to the C implementation in object.c and register it as CImpl. Preserve managed-object behavior with a narrow GraalPyPrivate_Object_IsTrue raw-pointer Java fallback implemented as a static CApiBuiltin method, matching the existing static-helper pattern instead of introducing a CApiBuiltin node class. Verification: mx python-jvm passed (mxbuild/buildlog-20260505-143336.html). JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest mx python-svm passed for the final implementation (mxbuild/buildlog-20260505-150119.html). Focused cpyext tests passed: mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_functions.py::tests.cpyext.test_functions.TestPyObject.test_PyObject_IsTrue graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_functions.py::tests.cpyext.test_functions.TestPyObject.test_PyObject_Not graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_nb_bool.py::test_from_python; result: Ran 3 tests in 1.03s, OK (passed=3). Full test suite passed: mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/; result: Ran 2807 tests in 705.34s, OK (passed=2788, skipped=19). git diff --check passed. Temporary benchmark: added graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pyobject-istrue.py during measurement, covering Py_True, Py_False, Py_None, nb_bool, mapping length, sequence length, and default truthy native-object paths; removed it before commit per PLAN.org. Benchmark command shape: --experimental-options --engine.Compilation=false graalpython/com.oracle.graal.python.benchmarks/python/harness.py -r 3 -i 9 graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pyobject-istrue.py 5000000. Benchmark quietness: before baseline and current measurements, observed the machine with vmstat 5 13 for about a minute. The windows were mostly r=0 with 99-100% idle after the initial sample, and ps showed no active build/test process; Emacs was the top process at about 5.6% CPU. Baseline: native standalone built with the implementation patch temporarily reversed and copied to /tmp/graalpy-pyobject-istrue-baseline-20260505-145638. Raw durations: [4.215643798, 4.200535418, 4.220929938, 4.25846425, 4.179787468, 4.173013933, 4.190631835, 4.118770273, 4.025052785]. Median: 4.190631835 s; best: 4.025052785 s; worst: 4.25846425 s; spread: 0.233411465 s. Current: raw durations: [0.175453561, 0.179316962, 0.177278654, 0.168218284, 0.170692362, 0.173828127, 0.168950964, 0.180557498, 0.181329358]. Median: 0.175453561 s; best: 0.168218284 s; worst: 0.181329358 s; spread: 0.013111074 s. Conclusion: moving PyObject_IsTrue from the Java direct builtin to the C implementation improves this native no-compilation benchmark by about 23.88x by median time. --- .../com.oracle.graal.python.cext/src/object.c | 4 ++-- .../modules/cext/PythonCextObjectBuiltins.java | 12 +++++------- .../builtins/objects/cext/capi/CApiFunction.java | 1 + 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/object.c b/graalpython/com.oracle.graal.python.cext/src/object.c index 88d8ae5990..54df3cc003 100644 --- a/graalpython/com.oracle.graal.python.cext/src/object.c +++ b/graalpython/com.oracle.graal.python.cext/src/object.c @@ -1776,7 +1776,6 @@ PyObject_GenericSetDict(PyObject *obj, PyObject *value, void *context) } -#if 0 // GraalPy change /* Test a value used as condition, e.g., in a while or if statement. Return -1 if an error occurred */ @@ -1790,6 +1789,8 @@ PyObject_IsTrue(PyObject *v) return 0; if (v == Py_None) return 0; + if (points_to_py_handle_space(v)) + return GraalPyPrivate_Object_IsTrue(v); else if (Py_TYPE(v)->tp_as_number != NULL && Py_TYPE(v)->tp_as_number->nb_bool != NULL) res = (*Py_TYPE(v)->tp_as_number->nb_bool)(v); @@ -1804,7 +1805,6 @@ PyObject_IsTrue(PyObject *v) /* if it is negative, it should be either -1 or -2 */ return (res > 0) ? 1 : Py_SAFE_DOWNCAST(res, Py_ssize_t, int); } -#endif // GraalPy change /* equivalent of 'not v' Return -1 if an error occurred */ diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java index ad8c43c711..53b5275386 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java @@ -90,6 +90,7 @@ import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.HandleContext; import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.HandlePointerConverter; import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.NativeToPythonInternalNode; +import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.NativeToPythonNode; import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.PythonObjectReference; import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.UpdateHandleTableReferenceNode; import com.oracle.graal.python.builtins.objects.cext.structs.CFields; @@ -484,13 +485,10 @@ static long unhashable(Object obj, } } - @CApiBuiltin(ret = Int, args = {PyObject}, call = Direct) - abstract static class PyObject_IsTrue extends CApiUnaryBuiltinNode { - @Specialization - static int isTrue(Object obj, - @Cached PyObjectIsTrueNode isTrueNode) { - return isTrueNode.execute(null, obj) ? 1 : 0; - } + @CApiBuiltin(ret = Int, args = {PyObjectRawPointer}, call = Ignored) + static int GraalPyPrivate_Object_IsTrue(long objPtr) { + Object obj = NativeToPythonNode.executeRawUncached(objPtr); + return PyObjectIsTrueNode.executeUncached(obj) ? 1 : 0; } @CApiBuiltin(ret = PyObjectTransfer, args = {PyObject}, call = Direct) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java index 7e1c8e6ea0..7188212843 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java @@ -445,6 +445,7 @@ public final class CApiFunction { @CApiBuiltin(name = "PyObject_Init", ret = PyObject, args = {PyObject, PyTypeObject}, call = CImpl) @CApiBuiltin(name = "PyObject_InitVar", ret = PyVarObject, args = {PyVarObject, PyTypeObject, Py_ssize_t}, call = CImpl) @CApiBuiltin(name = "PyObject_IS_GC", ret = Int, args = {PyObject}, call = CImpl) + @CApiBuiltin(name = "PyObject_IsTrue", ret = Int, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyObject_Malloc", ret = Pointer, args = {SIZE_T}, call = CImpl) @CApiBuiltin(name = "PyObject_Not", ret = Int, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyObject_Print", ret = Int, args = {PyObject, FILE_PTR, Int}, call = CImpl) From 8d682e7aed7041e18a5654792c4469697e1d96f5 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Tue, 5 May 2026 18:12:49 +0200 Subject: [PATCH 03/17] Move PyIter_Check to C implementation Register PyIter_Check as a CImpl C API function and remove the Java direct builtin. The CPython iterator block in abstract.c is disabled in GraalPy, so add a compiled PyIter_Check definition outside that disabled block to provide the native symbol. Verification: mx python-jvm passed (mxbuild/buildlog-20260505-174000.html). JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest mx python-svm passed (mxbuild/buildlog-20260505-174622.html; current rebuilt for benchmarking at mxbuild/buildlog-20260505-180655.html). Focused cpyext test passed: mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_tp_slots.py::test_tp_iternext_not_implemented, Ran 1 tests in 0.10s, OK (passed=1). Full test suite passed: mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/, Ran 2807 tests in 679.17s, OK (passed=2788, skipped=19). Benchmark: temporarily added c-pyiter-check.py covering a native iterator with tp_iternext, a native sequence-like iterable that is not itself an iterator, and a native non-iterator object; removed the temporary benchmark before committing. Quietness checks before both measurements used vmstat 5 13 and top CPU process snapshots; windows were mostly r=0/r=1 with 98-100% idle after the initial sample, with Emacs the top process at about 7.7% CPU. Benchmark command shape: --experimental-options --engine.Compilation=false graalpython/com.oracle.graal.python.benchmarks/python/harness.py -r 3 -i 9 graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pyiter-check.py 30000000. Baseline used /tmp/graalpy-pyiter-check-baseline-20260505-180231 built with this implementation patch temporarily reversed. Baseline raw durations: [6.189263866, 6.309805407, 6.338300861, 6.279226039, 6.268628357, 6.309469413, 6.320189933, 6.335439071, 6.284029805]; median 6.309469413 s, best 6.189263866 s, worst 6.338300861 s, spread 0.149036995 s. Current raw durations: [0.126759258, 0.129113078, 0.129032669, 0.126353708, 0.131626644, 0.127087841, 0.126351596, 0.124293469, 0.124502626]; median 0.126759258 s, best 0.124293469 s, worst 0.131626644 s, spread 0.007333175 s. Conclusion: about 49.78x faster by median time. --- .../com.oracle.graal.python.cext/src/abstract.c | 4 ++-- .../modules/cext/PythonCextIterBuiltins.java | 15 +-------------- .../builtins/objects/cext/capi/CApiFunction.java | 1 + 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/abstract.c b/graalpython/com.oracle.graal.python.cext/src/abstract.c index 6e5e2c08c2..43d548843f 100644 --- a/graalpython/com.oracle.graal.python.cext/src/abstract.c +++ b/graalpython/com.oracle.graal.python.cext/src/abstract.c @@ -2924,7 +2924,7 @@ PyObject_GetAIter(PyObject *o) { } return it; } - +#endif // GraalPy change int PyIter_Check(PyObject *obj) { @@ -2932,7 +2932,7 @@ PyIter_Check(PyObject *obj) return (tp->tp_iternext != NULL && tp->tp_iternext != &_PyObject_NextNotImplemented); } - +#if 0 // GraalPy change int PyAIter_Check(PyObject *obj) { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextIterBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextIterBuiltins.java index d2f60fba14..3072377989 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextIterBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextIterBuiltins.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -41,7 +41,6 @@ package com.oracle.graal.python.builtins.modules.cext; import static com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiCallPath.Direct; -import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Int; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObject; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectTransfer; @@ -51,25 +50,13 @@ import com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiBuiltin; import com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiUnaryBuiltinNode; import com.oracle.graal.python.builtins.objects.iterator.PSequenceIterator; -import com.oracle.graal.python.lib.PyIterCheckNode; import com.oracle.graal.python.runtime.object.PFactory; import com.oracle.truffle.api.dsl.Bind; import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Specialization; -import com.oracle.truffle.api.nodes.Node; public final class PythonCextIterBuiltins { - @CApiBuiltin(ret = Int, args = {PyObject}, call = Direct) - abstract static class PyIter_Check extends CApiUnaryBuiltinNode { - @Specialization - static int check(Object obj, - @Bind Node inliningTarget, - @Cached PyIterCheckNode check) { - return check.execute(inliningTarget, obj) ? 1 : 0; - } - } - @CApiBuiltin(ret = PyObjectTransfer, args = {PyObject}, call = Direct) abstract static class PySeqIter_New extends CApiUnaryBuiltinNode { @Specialization diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java index 7188212843..68ea44f5bd 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java @@ -321,6 +321,7 @@ public final class CApiFunction { @CApiBuiltin(name = "PyInterpreterState_GetDict", ret = PyObject, args = {PyInterpreterState}, call = CImpl) @CApiBuiltin(name = "PyInterpreterState_GetID", ret = INT64_T, args = {PyInterpreterState}, call = CImpl) @CApiBuiltin(name = "PyInterpreterState_Main", ret = PyInterpreterState, args = {}, call = CImpl) + @CApiBuiltin(name = "PyIter_Check", ret = Int, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyIter_Send", ret = PySendResult, args = {PyObject, PyObject, PyObjectPtr}, call = CImpl) @CApiBuiltin(name = "PyList_SetItem", ret = Int, args = {PyObject, Py_ssize_t, PyObjectTransfer}, call = CImpl) @CApiBuiltin(name = "PyLong_AsDouble", ret = Double, args = {PyObject}, call = CImpl) From 0a18a7d12bf7b75909c9e4aef50103937ef7a558 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Tue, 5 May 2026 18:50:19 +0200 Subject: [PATCH 04/17] Move PyObject_GetIter to C implementation Move the public PyObject_GetIter entry point from the Java direct builtin to the C implementation in abstract.c and register it as CImpl in CApiFunction. The compiled C implementation lives outside the disabled CPython iterator block, like PyIter_Check. It uses the native tp_iter / PySequence_Check / PySeqIter_New logic for native objects and preserves managed-object behavior through a narrow GraalPyPrivate_Object_GetIter raw-pointer Java fallback. Temporary benchmark: added graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pyobject-getiter.py covering a native tp_iter object, sequence fallback through PySeqIter_New, and one non-iterable TypeError correctness check; removed it before committing per PLAN.org. Verification: mx python-jvm passed (mxbuild/buildlog-20260505-181713.html). Focused cpyext tests passed: mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_functions.py::tests.cpyext.test_functions.TestPyObject.test_PyObject_GetIter graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_functions.py::tests.cpyext.test_functions.TestPyObject.test_PyObject_SelfIter graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_tp_slots.py::test_tp_iter_iternext_calls; result: Ran 3 tests in 0.45s, OK (passed=3). JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest mx python-svm passed (mxbuild/buildlog-20260505-182348.html; current rebuilt for benchmarking at mxbuild/buildlog-20260505-184436.html). Full suite passed: mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/; result: Ran 2807 tests in 700.30s, OK (passed=2788, skipped=19). Benchmark quietness: before baseline and current measurements, observed the machine with vmstat 5 13 for about a minute. Both windows were mostly r=0 with 99-100% idle after the initial sample, and no active build/test process was visible among top CPU consumers; Emacs was the top process at about 8.2% CPU. Benchmark command shape: --experimental-options --engine.Compilation=false graalpython/com.oracle.graal.python.benchmarks/python/harness.py -r 3 -i 9 graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pyobject-getiter.py 5000000. Baseline: native standalone built with this implementation patch temporarily reversed and copied to /tmp/graalpy-pyobject-getiter-baseline-20260505-184016. Raw durations: [5.662864924, 5.77716436, 5.865396065, 5.926168834, 5.827736089, 5.911669503, 5.892751251, 5.92403813, 5.842762165]. Median: 5.865396065 s; best: 5.662864924 s; worst: 5.926168834 s; spread: 0.26330391 s. Current: raw durations: [3.84010954, 3.878019773, 3.899857059, 3.925100635, 3.966473596, 3.925557547, 3.915327162, 3.934049151, 3.905884266]. Median: 3.915327162 s; best: 3.84010954 s; worst: 3.966473596 s; spread: 0.126364056 s. Conclusion: moving PyObject_GetIter from the Java direct builtin to the hybrid C implementation improves this native no-compilation benchmark by about 1.50x by median time. --- .../com.oracle.graal.python.cext/src/abstract.c | 8 ++++++-- .../modules/cext/PythonCextObjectBuiltins.java | 14 ++++++-------- .../builtins/objects/cext/capi/CApiFunction.java | 1 + 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/abstract.c b/graalpython/com.oracle.graal.python.cext/src/abstract.c index 43d548843f..c1ebb2d4bd 100644 --- a/graalpython/com.oracle.graal.python.cext/src/abstract.c +++ b/graalpython/com.oracle.graal.python.cext/src/abstract.c @@ -2880,10 +2880,14 @@ _PyObject_RealIsSubclass(PyObject *derived, PyObject *cls) return recursive_issubclass(derived, cls); } - +#endif // GraalPy change PyObject * PyObject_GetIter(PyObject *o) { + if (points_to_py_handle_space(o)) { + return GraalPyPrivate_Object_GetIter(o); + } + PyTypeObject *t = Py_TYPE(o); getiterfunc f; @@ -2905,7 +2909,7 @@ PyObject_GetIter(PyObject *o) return res; } } - +#if 0 // GraalPy change PyObject * PyObject_GetAIter(PyObject *o) { PyTypeObject *t = Py_TYPE(o); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java index 53b5275386..0565c8474a 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java @@ -92,6 +92,7 @@ import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.NativeToPythonInternalNode; import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.NativeToPythonNode; import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.PythonObjectReference; +import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.PythonToNativeNewRefNode; import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.UpdateHandleTableReferenceNode; import com.oracle.graal.python.builtins.objects.cext.structs.CFields; import com.oracle.graal.python.builtins.objects.cext.structs.CStructAccess; @@ -675,14 +676,11 @@ static Object ascii(Object obj, Object spec, } } - @CApiBuiltin(ret = PyObjectTransfer, args = {PyObject}, call = Direct) - abstract static class PyObject_GetIter extends CApiUnaryBuiltinNode { - @Specialization - static Object iter(Object object, - @Bind Node inliningTarget, - @Cached PyObjectGetIter getIter) { - return getIter.execute(null, inliningTarget, object); - } + @CApiBuiltin(ret = PyObjectRawPointer, args = {PyObjectRawPointer}, call = Ignored) + static long GraalPyPrivate_Object_GetIter(long objectPtr) { + Object object = NativeToPythonNode.executeRawUncached(objectPtr); + Object result = PyObjectGetIter.executeUncached(object); + return PythonToNativeNewRefNode.executeLongUncached(result); } @CApiBuiltin(ret = Py_hash_t, args = {PyObject}, call = Direct) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java index 68ea44f5bd..47516f0c1a 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java @@ -443,6 +443,7 @@ public final class CApiFunction { @CApiBuiltin(name = "PyObject_GetAttr", ret = PyObject, args = {PyObject, PyObject}, call = CImpl) @CApiBuiltin(name = "PyObject_GetAttrString", ret = PyObject, args = {PyObject, ConstCharPtrAsTruffleString}, call = CImpl) @CApiBuiltin(name = "PyObject_GetBuffer", ret = Int, args = {PyObject, PY_BUFFER_PTR, Int}, call = CImpl) + @CApiBuiltin(name = "PyObject_GetIter", ret = PyObject, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyObject_Init", ret = PyObject, args = {PyObject, PyTypeObject}, call = CImpl) @CApiBuiltin(name = "PyObject_InitVar", ret = PyVarObject, args = {PyVarObject, PyTypeObject, Py_ssize_t}, call = CImpl) @CApiBuiltin(name = "PyObject_IS_GC", ret = Int, args = {PyObject}, call = CImpl) From 42949a2b33222b952c97ce47c19324a3a1291603 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Tue, 5 May 2026 20:57:03 +0200 Subject: [PATCH 05/17] Move PyDict_Size to C implementation Move PyDict_Size out of the Java direct builtin registry and register it as a CImpl entry in CApiFunction. The existing upstream PyDict_Size body in dictobject.c is inside a disabled GraalPy #if 0 block, so add the active C implementation in the compiled GraalPy section near dict_dealloc. The implementation keeps wrong-type behavior through PyErr_BadInternalCall, uses GraalPyPrivate_Object_Size for managed dict and dict subclass objects, and reads ma_used directly for native dict objects. Verification: mx python-jvm passed (mxbuild/buildlog-20260505-204056.html before final JVM rebuild; final current rebuild passed at mxbuild/buildlog-20260505-205446.html). Focused cpyext command passed: mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_dict.py::tests.cpyext.test_dict.TestPyDict.test_PyDict_Size graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_functions.py::tests.cpyext.test_functions.TestPyObject.test_PyObject_Call. Full dict cpyext file passed: mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_dict.py (22 tests). Native builds passed with JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest mx python-svm for current (mxbuild/buildlog-20260505-204651.html) and for the temporarily reversed baseline (mxbuild/buildlog-20260505-205117.html). Temporary benchmark: added graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pydict-size.py during investigation only, covering managed exact dict, C-created exact dict, dict subclass correctness, and wrong-type SystemError correctness; removed it before commit per PLAN.org. Quietness check before measurement: vmstat 5 13 over about one minute showed 100% idle after the initial historical line and run queue 0 or 1; ps showed no active build/test CPU-heavy process. Benchmark command: --experimental-options --engine.Compilation=false graalpython/com.oracle.graal.python.benchmarks/python/harness.py -r 3 -i 9 graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pydict-size.py 10000000. Baseline Java-direct standalone was built with the PyDict_Size patch temporarily reversed and copied to /tmp/graalpy-pydict-size-baseline-20260505-2051. Baseline raw durations: [1.432521117, 1.378349267, 1.390894927, 1.38433802, 1.443656571, 1.43790804, 1.43800257, 1.413966472, 1.417834157]; best 1.378s, worst 1.444s, average 1.415s, median 1.418s. Current CImpl standalone was /tmp/graalpy-pydict-size-current-20260505-2047. Current raw durations: [0.941734285, 0.937482254, 0.941674476, 0.919298855, 0.928701666, 0.935161315, 0.948869297, 0.941842147, 0.940401473]; best 0.919s, worst 0.949s, average 0.937s, median 0.940s. Conclusion: about 1.51x faster by median time. --- .../src/dictobject.c | 9 ++++++--- .../modules/cext/PythonCextDictBuiltins.java | 16 ---------------- .../builtins/objects/cext/capi/CApiFunction.java | 1 + 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/dictobject.c b/graalpython/com.oracle.graal.python.cext/src/dictobject.c index 1abc5634f3..14faebd390 100644 --- a/graalpython/com.oracle.graal.python.cext/src/dictobject.c +++ b/graalpython/com.oracle.graal.python.cext/src/dictobject.c @@ -1,4 +1,4 @@ -/* Copyright (c) 2024, 2025, Oracle and/or its affiliates. +/* Copyright (c) 2024, 2026, Oracle and/or its affiliates. * Copyright (C) 1996-2024 Python Software Foundation * * Licensed under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 @@ -3086,7 +3086,7 @@ PyDict_Copy(PyObject *o) Py_DECREF(copy); return NULL; } - +#endif // GraalPy change Py_ssize_t PyDict_Size(PyObject *mp) { @@ -3094,9 +3094,12 @@ PyDict_Size(PyObject *mp) PyErr_BadInternalCall(); return -1; } + if (points_to_py_handle_space(mp)) { + return GraalPyPrivate_Object_Size(mp); + } return ((PyDictObject *)mp)->ma_used; } - +#if 0 // GraalPy change PyObject * PyDict_Keys(PyObject *mp) { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextDictBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextDictBuiltins.java index ae2ce9e9f2..43b78b22ff 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextDictBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextDictBuiltins.java @@ -53,7 +53,6 @@ import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectPtr; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectTransfer; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Py_hash_t; -import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Py_ssize_t; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Void; import static com.oracle.graal.python.runtime.nativeaccess.NativeMemory.NULLPTR; import static com.oracle.graal.python.runtime.nativeaccess.NativeMemory.readLong; @@ -275,21 +274,6 @@ public Object fallback(Object dict, @SuppressWarnings("unused") Object key, @Sup } } - @CApiBuiltin(ret = Py_ssize_t, args = {PyObject}, call = Direct) - abstract static class PyDict_Size extends CApiUnaryBuiltinNode { - @Specialization - static long size(PDict dict, - @Bind Node inliningTarget, - @Cached HashingStorageLen lenNode) { - return lenNode.execute(inliningTarget, dict.getDictStorage()); - } - - @Fallback - public long fallback(Object dict) { - throw raiseFallback(dict, PythonBuiltinClassType.PDict); - } - } - @CApiBuiltin(ret = PyObjectTransfer, args = {PyObject}, call = Direct) abstract static class PyDict_Copy extends CApiUnaryBuiltinNode { @Specialization diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java index 47516f0c1a..8095aafcad 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java @@ -250,6 +250,7 @@ public final class CApiFunction { @CApiBuiltin(name = "PyDict_DelItemString", ret = Int, args = {PyObject, ConstCharPtrAsTruffleString}, call = CImpl) @CApiBuiltin(name = "PyDict_GetItemString", ret = PyObject, args = {PyObject, ConstCharPtrAsTruffleString}, call = CImpl) @CApiBuiltin(name = "PyDict_Next", ret = Int, args = {PyObject, PY_SSIZE_T_PTR, PyObjectPtr, PyObjectPtr}, call = CImpl) + @CApiBuiltin(name = "PyDict_Size", ret = Py_ssize_t, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyDict_SetItemString", ret = Int, args = {PyObject, ConstCharPtrAsTruffleString, PyObject}, call = CImpl) @CApiBuiltin(name = "PyErr_BadArgument", ret = Int, args = {}, call = CImpl) @CApiBuiltin(name = "PyErr_BadInternalCall", ret = Void, args = {}, call = CImpl) From 948b562141eee210a5486d4c40c5820bfc6490e5 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Tue, 5 May 2026 22:52:31 +0200 Subject: [PATCH 06/17] Move _PyNumber_Index to C implementation Move _PyNumber_Index from the shared Java-direct PyNumber_Index builtin to the C implementation in abstract.c and register only _PyNumber_Index as CImpl. Keep public PyNumber_Index Java-direct for the separate TODO so exact-int copy semantics remain unchanged there. Add GraalPyPrivate_PyNumber_Index as the managed-object fallback. It preserves CPython _PyNumber_Index behavior: existing int subclasses are returned directly, __index__ results may be int subclasses, and non-int results raise TypeError. Add cpyext coverage that distinguishes exact int from bool and custom int-subclass results. Verification: mx python-jvm passed (mxbuild/buildlog-20260505-222959.html). JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest mx python-svm passed (mxbuild/buildlog-20260505-223449.html; final native focused rebuild at mxbuild/buildlog-20260505-223828.html). Focused JVM tests passed: mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_abstract.py::tests.cpyext.test_abstract.TestAbstract.test__PyNumber_Index graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_abstract.py::tests.cpyext.test_abstract.TestAbstract.test_PyNumber_Index, Ran 2 tests in 1.84s, OK (passed=2). Focused native tests passed with pinned JAVA_HOME/LATEST_JAVA_HOME and GRAALPYTEST_ALLOW_NO_JAVA_ASSERTIONS=true: mx graalpytest --svm for the same two tests, Ran 2 tests in 1.38s, OK (passed=2). Full suite passed: mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/ --mx-report /tmp/pynumber-index-full-report.json, Ran 2808 tests in 694.67s, OK (passed=2789, skipped=19). Benchmark: temporarily added graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pynumber-index.py and removed it before committing. Command shape: --experimental-options --engine.Compilation=false graalpython/com.oracle.graal.python.benchmarks/python/harness.py -r 3 -i 9 graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pynumber-index.py 10000000. Quietness was checked with vmstat 5 13 before current and baseline; both windows were mostly r=0 with 99-100% idle after the initial historical sample and no active build/test CPU-heavy process visible; Emacs was the top sustained process at about 16.5% of one CPU. Current CImpl measurement used /tmp/graalpy-pynumber-index-current-20260505-2152. Raw durations: [0.399217322, 0.386455915, 0.390759825, 0.387381291, 0.404902442, 0.385551238, 0.385262956, 0.385844885, 0.392532917]. Best 0.385s, worst 0.405s, average 0.391s, median 0.387s. Baseline Java-direct measurement used /tmp/graalpy-pynumber-index-baseline-20260505-2225. Raw durations: [1.699626159, 1.693498868, 1.689648931, 1.694836749, 1.695359309, 1.693872535, 1.702244645, 1.693211745, 1.721866315]. Best 1.690s, worst 1.722s, average 1.698s, median 1.695s. The benchmark setup initially caught a semantic difference in the baseline: Java-direct _PyNumber_Index returned exact int for bool/int-subclass cases where CPython preserves the subclass. The timing setup was relaxed to measure the old path, while the permanent cpyext test keeps the stricter behavior check. Conclusion: moving _PyNumber_Index to CImpl improves this native no-compilation benchmark by about 4.38x by median time and fixes the private API's int-subclass return semantics. --- .../src/abstract.c | 8 +++- .../src/tests/cpyext/test_abstract.py | 38 ++++++++++++++++++- .../cext/PythonCextAbstractBuiltins.java | 30 ++++++++++++++- .../objects/cext/capi/CApiFunction.java | 1 + 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/abstract.c b/graalpython/com.oracle.graal.python.cext/src/abstract.c index c1ebb2d4bd..5bf5e09735 100644 --- a/graalpython/com.oracle.graal.python.cext/src/abstract.c +++ b/graalpython/com.oracle.graal.python.cext/src/abstract.c @@ -1453,8 +1453,6 @@ PyIndex_Check(PyObject *obj) return _PyIndex_Check(obj); } - -#if 0 // GraalPy change /* Return a Python int from the object item. Can return an instance of int subclass. Raise TypeError if the result is not an int @@ -1467,6 +1465,11 @@ _PyNumber_Index(PyObject *item) return null_error(); } + // GraalPy change: upcall for managed objects + if (points_to_py_handle_space(item)) { + return GraalPyPrivate_PyNumber_Index(item); + } + if (PyLong_Check(item)) { return Py_NewRef(item); } @@ -1502,6 +1505,7 @@ _PyNumber_Index(PyObject *item) return result; } +#if 0 // GraalPy change /* Return an exact Python int from the object item. Raise TypeError if the result is not an int or if the object cannot be interpreted as an index. diff --git a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_abstract.py b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_abstract.py index 70553a19ff..0e185cd0f2 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_abstract.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_abstract.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # The Universal Permissive License (UPL), Version 1.0 @@ -90,6 +90,13 @@ def _reference_index(args): return result +def _reference_private_index(args): + if isinstance(args[0], int): + return type(args[0]).__name__, args[0] + result = _reference_index(args) + return type(result).__name__, result + + def _reference_asssize_t(args): v = args[0] err = args[1] @@ -1025,6 +1032,35 @@ class TestAbstract(CPyExtTestCase): cmpfunc=unhandled_error_compare ) + test__PyNumber_Index = CPyExtFunction( + _reference_private_index, + lambda: ( + (0,), + (True,), + (DummyIndexable(),), + (DummyIntSubclass(),), + (NoNumber(),), + ), + resultspec="O", + argspec='O', + arguments=["PyObject* v"], + code=''' + PyObject* wrap__PyNumber_Index(PyObject* v) { + PyObject* result = _PyNumber_Index(v); + if (result == NULL) { + return NULL; + } + PyObject* type_name = PyUnicode_FromString(Py_TYPE(result)->tp_name); + PyObject* tuple = PyTuple_Pack(2, type_name, result); + Py_DECREF(type_name); + Py_DECREF(result); + return tuple; + } + ''', + callfunction="wrap__PyNumber_Index", + cmpfunc=unhandled_error_compare + ) + test_PyNumber_AsSsize_t = CPyExtFunction( _reference_asssize_t, lambda: ( diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextAbstractBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextAbstractBuiltins.java index 2c9313427c..e0f7953b1f 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextAbstractBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextAbstractBuiltins.java @@ -40,6 +40,7 @@ */ package com.oracle.graal.python.builtins.modules.cext; +import static com.oracle.graal.python.builtins.PythonBuiltinClassType.DeprecationWarning; import static com.oracle.graal.python.builtins.PythonBuiltinClassType.OverflowError; import static com.oracle.graal.python.builtins.PythonBuiltinClassType.SystemError; import static com.oracle.graal.python.builtins.PythonBuiltinClassType.TypeError; @@ -63,12 +64,14 @@ import static com.oracle.graal.python.nodes.SpecialMethodNames.T_KEYS; import static com.oracle.graal.python.nodes.SpecialMethodNames.T_VALUES; import static com.oracle.graal.python.nodes.SpecialMethodNames.T___GETITEM__; +import static com.oracle.graal.python.nodes.SpecialMethodNames.T___INDEX__; import static com.oracle.graal.python.runtime.PythonContext.NATIVE_NULL; import com.oracle.graal.python.builtins.PythonBuiltinClassType; import com.oracle.graal.python.builtins.modules.BuiltinFunctions.BinNode; import com.oracle.graal.python.builtins.modules.BuiltinFunctions.HexNode; import com.oracle.graal.python.builtins.modules.BuiltinFunctions.OctNode; +import com.oracle.graal.python.builtins.modules.WarningsModuleBuiltins; import com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiBinaryBuiltinNode; import com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiBuiltin; import com.oracle.graal.python.builtins.objects.PNone; @@ -91,9 +94,12 @@ import com.oracle.graal.python.builtins.objects.type.TypeNodes.IsSameTypeNode; import com.oracle.graal.python.builtins.objects.type.TypeNodes.IsTypeNode; import com.oracle.graal.python.builtins.objects.type.slots.TpSlotMpAssSubscript.CallSlotMpAssSubscriptNode; +import com.oracle.graal.python.builtins.objects.type.slots.TpSlotUnaryFunc.CallSlotUnaryNode; import com.oracle.graal.python.lib.IteratorExhausted; import com.oracle.graal.python.lib.PyIterCheckNode; import com.oracle.graal.python.lib.PyIterNextNode; +import com.oracle.graal.python.lib.PyLongCheckExactNode; +import com.oracle.graal.python.lib.PyLongCheckNode; import com.oracle.graal.python.lib.PyNumberAddNode; import com.oracle.graal.python.lib.PyNumberAndNode; import com.oracle.graal.python.lib.PyNumberDivmodNode; @@ -167,7 +173,6 @@ public final class PythonCextAbstractBuiltins { private static final TruffleLogger PY_OBJECT_SET_DOC_LOGGER = CApiContext.getLogger(PythonCextAbstractBuiltins.class); /////// PyNumber /////// - @CApiBuiltin(name = "_PyNumber_Index", ret = PyObjectRawPointer, args = {PyObjectRawPointer}, call = Direct, acquireGil = false) @CApiBuiltin(ret = PyObjectRawPointer, args = {PyObjectRawPointer}, call = Direct, acquireGil = false) static long PyNumber_Index(long objPtr) { Object obj = NativeToPythonNode.executeRawUncached(objPtr); @@ -176,6 +181,29 @@ static long PyNumber_Index(long objPtr) { return PythonToNativeNewRefNode.executeLongUncached(result); } + @CApiBuiltin(ret = PyObjectRawPointer, args = {PyObjectRawPointer}, call = Ignored, acquireGil = false) + static long GraalPyPrivate_PyNumber_Index(long objPtr) { + Object obj = NativeToPythonNode.executeRawUncached(objPtr); + checkNonNullArgUncached(obj); + if (PyLongCheckNode.executeUncached(obj)) { + return PythonToNativeNewRefNode.executeLongUncached(obj); + } + TpSlots slots = GetObjectSlotsNode.executeUncached(obj); + if (slots.nb_index() == null) { + throw PRaiseNode.raiseStatic(null, TypeError, ErrorMessages.OBJ_CANNOT_BE_INTERPRETED_AS_INTEGER, obj); + } + Object result = CallSlotUnaryNode.executeUncached(slots.nb_index(), obj); + if (PyLongCheckExactNode.executeUncached(result)) { + return PythonToNativeNewRefNode.executeLongUncached(result); + } + if (!PyLongCheckNode.executeUncached(result)) { + throw PRaiseNode.raiseStatic(null, TypeError, ErrorMessages.INDEX_RETURNED_NON_INT, result); + } + WarningsModuleBuiltins.WarnNode.getUncached().warnFormat(null, null, DeprecationWarning, 1, + ErrorMessages.WARN_P_RETURNED_NON_P, obj, T___INDEX__, "int", result, "int"); + return PythonToNativeNewRefNode.executeLongUncached(result); + } + @CApiBuiltin(ret = PyObjectRawPointer, args = {PyObjectRawPointer}, call = Ignored, acquireGil = false) static long GraalPyPrivate_PyNumber_Long(long objectPtr) { Object object = NativeToPythonNode.executeRawUncached(objectPtr); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java index 8095aafcad..87123dd879 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java @@ -381,6 +381,7 @@ public final class CApiFunction { @CApiBuiltin(name = "PyNumber_Divmod", ret = PyObject, args = {PyObject, PyObject}, call = CImpl) @CApiBuiltin(name = "PyNumber_Float", ret = PyObject, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyNumber_Long", ret = PyObject, args = {PyObject}, call = CImpl) + @CApiBuiltin(name = "_PyNumber_Index", ret = PyObject, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyNumber_FloorDivide", ret = PyObject, args = {PyObject, PyObject}, call = CImpl) @CApiBuiltin(name = "PyNumber_InPlaceAdd", ret = PyObject, args = {PyObject, PyObject}, call = CImpl) @CApiBuiltin(name = "PyNumber_InPlaceAnd", ret = PyObject, args = {PyObject, PyObject}, call = CImpl) From 9acbc647b185976d0e8eefd851157abcc9343a1c Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Tue, 5 May 2026 23:48:26 +0200 Subject: [PATCH 07/17] Move PyNumber_Index to C implementation Move PyNumber_Index from the Java-direct C API builtin to the existing C implementation in abstract.c and register it as CImpl in CApiFunction. Preserve CPython exact-int copy semantics for managed int subclasses with a narrow GraalPyPrivate_PyNumber_IndexCopy fallback used only when the C implementation receives a managed non-exact long result. Native non-exact longs still use _PyLong_Copy. Tighten cpyext coverage for PyNumber_Index to return (Py_TYPE(result)->tp_name, result), distinguishing exact int results from bool and int subclasses. Add coverage for an object whose __index__ returns an int subclass. Full-suite verification initially caught that calling _PyLong_Copy directly on a managed int-subclass result reaches the not-implemented C API route. The managed-copy fallback fixes this while keeping the public entry point in C. Verification: mx python-jvm passed (final source rebuild at mxbuild/buildlog-20260505-233849.html; earlier validation rebuild after the managed-copy fix at mxbuild/buildlog-20260505-230417.html). JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest mx python-svm passed (final source rebuild at mxbuild/buildlog-20260505-234340.html; final native focused rebuild at mxbuild/buildlog-20260505-234724.html). Focused JVM tests passed: mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_abstract.py::tests.cpyext.test_abstract.TestAbstract.test_PyNumber_Index graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_abstract.py::tests.cpyext.test_abstract.TestAbstract.test__PyNumber_Index graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_abstract.py::tests.cpyext.test_abstract.TestAbstract.test_PyNumber_AsSsize_t, Ran 3 tests in 1.77s, OK (passed=3). Focused native tests passed with pinned JAVA_HOME/LATEST_JAVA_HOME and GRAALPYTEST_ALLOW_NO_JAVA_ASSERTIONS=true for the same tests, Ran 3 tests in 1.80s, OK (passed=3). Full suite passed: mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/ --mx-report /tmp/pynumber-index-public-full-report.json, Ran 2808 tests in 729.54s, OK (passed=2789, skipped=19). Benchmark: temporarily added graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pynumber-index-public.py and removed it before committing. The benchmark measured exact int and native nb_index exact-int paths in the timed loop, with one-time correctness checks for non-index TypeError and managed __index__ returning an int subclass that must be copied to exact int. A first attempt to time the int-subclass-return path every iteration was stopped because repeated deprecation-warning processing dominated runtime. Benchmark command: --experimental-options --engine.Compilation=false graalpython/com.oracle.graal.python.benchmarks/python/harness.py -r 3 -i 9 graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pynumber-index-public.py 10000000. Quietness was checked with vmstat 5 13 before current and baseline. The current window was mostly r=0 with 99-100% idle after the initial historical sample; the baseline window was r=0/r=1 with 99-100% idle after the initial historical sample. ps showed no active build/test CPU-heavy process; Emacs was the top sustained process at about 16% of one CPU. Current CImpl measurement used /tmp/graalpy-pynumber-index-public-current-20260505-2327. Raw durations: [0.596066757, 0.611307405, 0.616138294, 0.601266838, 0.604464302, 0.610334569, 0.608268694, 0.609358989, 0.592895327]. Best 0.593s, worst 0.616s, average 0.606s, median 0.608s. Baseline Java-direct measurement used /tmp/graalpy-pynumber-index-public-baseline-20260505-2335. Raw durations: [1.887633857, 1.898408786, 1.939036876, 1.912759437, 1.902811571, 1.891698046, 1.914234673, 1.913143756, 1.893450307]. Best 1.888s, worst 1.939s, average 1.906s, median 1.903s. Conclusion: moving PyNumber_Index to CImpl improves this native no-compilation benchmark by about 3.13x by median time while preserving exact-int copy semantics. --- .../src/abstract.c | 8 +++-- .../src/tests/cpyext/test_abstract.py | 30 ++++++++++++++++++- .../cext/PythonCextAbstractBuiltins.java | 16 +++++----- .../objects/cext/capi/CApiFunction.java | 1 + 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/abstract.c b/graalpython/com.oracle.graal.python.cext/src/abstract.c index 5bf5e09735..b2facc99af 100644 --- a/graalpython/com.oracle.graal.python.cext/src/abstract.c +++ b/graalpython/com.oracle.graal.python.cext/src/abstract.c @@ -1505,7 +1505,6 @@ _PyNumber_Index(PyObject *item) return result; } -#if 0 // GraalPy change /* Return an exact Python int from the object item. Raise TypeError if the result is not an int or if the object cannot be interpreted as an index. @@ -1515,11 +1514,14 @@ PyNumber_Index(PyObject *item) { PyObject *result = _PyNumber_Index(item); if (result != NULL && !PyLong_CheckExact(result)) { - Py_SETREF(result, _PyLong_Copy((PyLongObject *)result)); + if (points_to_py_handle_space(result)) { + Py_SETREF(result, GraalPyPrivate_PyNumber_IndexCopy(result)); + } else { + Py_SETREF(result, _PyLong_Copy((PyLongObject *)result)); + } } return result; } -#endif // GraalPy change /* Return an error on Overflow only if err is not NULL*/ diff --git a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_abstract.py b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_abstract.py index 0e185cd0f2..383ea8038b 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_abstract.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_abstract.py @@ -90,6 +90,12 @@ def _reference_index(args): return result +def _reference_index_exact(args): + result = _reference_index(args) + result = result.__index__() + return type(result).__name__, result + + def _reference_private_index(args): if isinstance(args[0], int): return type(args[0]).__name__, args[0] @@ -244,6 +250,12 @@ def __index__(self): return 0xCAFE +class DummyIndexableIntSubclass(): + + def __index__(self): + return DummyIntSubclass() + + class DummyIntSubclass(int): def __int__(self): @@ -1013,9 +1025,10 @@ class TestAbstract(CPyExtTestCase): ) test_PyNumber_Index = CPyExtFunction( - _reference_index, + _reference_index_exact, lambda: ( (0,), + (True,), (1,), (-1,), (1.0,), @@ -1023,12 +1036,27 @@ class TestAbstract(CPyExtTestCase): (0x7FFFFFFF,), (0x7FFFFFFFFFFFFFFF,), (DummyIntable(),), + (DummyIndexableIntSubclass(),), (DummyIntSubclass(),), (NoNumber(),), ), resultspec="O", argspec='O', arguments=["PyObject* v"], + code=''' + PyObject* wrap_PyNumber_Index(PyObject* v) { + PyObject* result = PyNumber_Index(v); + if (result == NULL) { + return NULL; + } + PyObject* type_name = PyUnicode_FromString(Py_TYPE(result)->tp_name); + PyObject* tuple = PyTuple_Pack(2, type_name, result); + Py_DECREF(type_name); + Py_DECREF(result); + return tuple; + } + ''', + callfunction="wrap_PyNumber_Index", cmpfunc=unhandled_error_compare ) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextAbstractBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextAbstractBuiltins.java index e0f7953b1f..e2ddc17e46 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextAbstractBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextAbstractBuiltins.java @@ -100,6 +100,7 @@ import com.oracle.graal.python.lib.PyIterNextNode; import com.oracle.graal.python.lib.PyLongCheckExactNode; import com.oracle.graal.python.lib.PyLongCheckNode; +import com.oracle.graal.python.lib.PyLongCopyNodeGen; import com.oracle.graal.python.lib.PyNumberAddNode; import com.oracle.graal.python.lib.PyNumberAndNode; import com.oracle.graal.python.lib.PyNumberDivmodNode; @@ -173,14 +174,6 @@ public final class PythonCextAbstractBuiltins { private static final TruffleLogger PY_OBJECT_SET_DOC_LOGGER = CApiContext.getLogger(PythonCextAbstractBuiltins.class); /////// PyNumber /////// - @CApiBuiltin(ret = PyObjectRawPointer, args = {PyObjectRawPointer}, call = Direct, acquireGil = false) - static long PyNumber_Index(long objPtr) { - Object obj = NativeToPythonNode.executeRawUncached(objPtr); - checkNonNullArgUncached(obj); - Object result = PyNumberIndexNode.executeUncached(obj); - return PythonToNativeNewRefNode.executeLongUncached(result); - } - @CApiBuiltin(ret = PyObjectRawPointer, args = {PyObjectRawPointer}, call = Ignored, acquireGil = false) static long GraalPyPrivate_PyNumber_Index(long objPtr) { Object obj = NativeToPythonNode.executeRawUncached(objPtr); @@ -204,6 +197,13 @@ static long GraalPyPrivate_PyNumber_Index(long objPtr) { return PythonToNativeNewRefNode.executeLongUncached(result); } + @CApiBuiltin(ret = PyObjectRawPointer, args = {PyObjectRawPointer}, call = Ignored, acquireGil = false) + static long GraalPyPrivate_PyNumber_IndexCopy(long objPtr) { + Object obj = NativeToPythonNode.executeRawUncached(objPtr); + Object result = PyLongCopyNodeGen.getUncached().execute(null, obj); + return PythonToNativeNewRefNode.executeLongUncached(result); + } + @CApiBuiltin(ret = PyObjectRawPointer, args = {PyObjectRawPointer}, call = Ignored, acquireGil = false) static long GraalPyPrivate_PyNumber_Long(long objectPtr) { Object object = NativeToPythonNode.executeRawUncached(objectPtr); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java index 87123dd879..f0c981c1e0 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java @@ -382,6 +382,7 @@ public final class CApiFunction { @CApiBuiltin(name = "PyNumber_Float", ret = PyObject, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyNumber_Long", ret = PyObject, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "_PyNumber_Index", ret = PyObject, args = {PyObject}, call = CImpl) + @CApiBuiltin(name = "PyNumber_Index", ret = PyObject, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyNumber_FloorDivide", ret = PyObject, args = {PyObject, PyObject}, call = CImpl) @CApiBuiltin(name = "PyNumber_InPlaceAdd", ret = PyObject, args = {PyObject, PyObject}, call = CImpl) @CApiBuiltin(name = "PyNumber_InPlaceAnd", ret = PyObject, args = {PyObject, PyObject}, call = CImpl) From 85a814419ed356ab828c8a428b672bfb68b51110 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 6 May 2026 00:41:50 +0200 Subject: [PATCH 08/17] Move PyLong_FromUnsignedLong to C implementation Register PyLong_FromUnsignedLong as a CImpl entry point and enable the public C symbol in longobject.c. The C path returns tagged int32 pointers directly for small unsigned values and calls the private GraalPyPrivate_Long_FromUnsignedLong static C API helper for values requiring an unsigned multi-digit Python int. A pure CPython-style C implementation is still blocked because _PyLong_New is not available through this route (Function not implemented in GraalPy: _PyLong_New), so the large-value path intentionally falls back to Java object creation. Add cpyext coverage for PyLong_FromUnsignedLong(0), PyLong_FromUnsignedLong(1), and PyLong_FromUnsignedLong(ULONG_MAX). Verification: mx python-jvm passed (mxbuild/buildlog-20260506-003157.html). mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromUnsignedLong graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromSize_t graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromSsize_t passed: 3 tests, OK. JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest mx python-svm passed (mxbuild/buildlog-20260506-003700.html). JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest GRAALPYTEST_ALLOW_NO_JAVA_ASSERTIONS=true mx graalpytest --svm graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromUnsignedLong graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromSize_t graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromSsize_t passed: 3 tests, OK (mxbuild/buildlog-20260506-004023.html). Benchmarks: temporarily added graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pylong-from-unsigned-long.py and removed it before committing. Before each measured run, vmstat 5 13 observed the machine for about one minute; after the initial historical line the system stayed around 99-100% idle for current, baseline, and clean-baseline runs, and ps showed no active build/test CPU-heavy process. Command shape: --experimental-options --engine.Compilation=false graalpython/com.oracle.graal.python.benchmarks/python/harness.py -r 3 -i 9 graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pylong-from-unsigned-long.py , with modes 0=small, 1=ULONG_MAX, 2=mixed. Benchmark results: mixed mode, 10,000,000 iterations: baseline median 7.462s (best 7.424s, worst 7.556s), current median 6.768s (best 6.573s, worst 6.841s), about 1.10x faster. Small-only mode, 100,000,000 iterations: baseline median 5.524s (best 5.476s, worst 5.626s), current median 0.137s (best 0.134s, worst 0.144s), about 40.35x faster. ULONG_MAX-only mode, 10,000,000 iterations: baseline median 6.876s (best 6.791s, worst 7.039s), current median 6.567s (best 6.503s, worst 6.689s), about 1.05x faster. --- .../src/longobject.c | 12 ++++++- .../src/tests/cpyext/test_long.py | 31 ++++++++++++++++++- .../modules/cext/PythonCextLongBuiltins.java | 14 ++++++++- .../objects/cext/capi/CApiFunction.java | 1 + 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/longobject.c b/graalpython/com.oracle.graal.python.cext/src/longobject.c index 58eb4997e7..82e036981e 100644 --- a/graalpython/com.oracle.graal.python.cext/src/longobject.c +++ b/graalpython/com.oracle.graal.python.cext/src/longobject.c @@ -335,8 +335,16 @@ PyLong_FromLong(long ival) return PyLong_FromLongLong((long long) ival); } -#if 0 // GraalPy change #define PYLONG_FROM_UINT(INT_TYPE, ival) \ + do { \ + if ((ival) <= INT32_MAX) { \ + return int32_to_pointer((int)(ival)); \ + } \ + return GraalPyPrivate_Long_FromUnsignedLong((unsigned long)(ival)); \ + } while(0) + +#if 0 // GraalPy change +#define PYLONG_FROM_UINT_CPYTHON(INT_TYPE, ival) \ do { \ if (IS_SMALL_UINT(ival)) { \ return get_small_int((sdigit)(ival)); \ @@ -359,6 +367,7 @@ PyLong_FromLong(long ival) } \ return (PyObject *)v; \ } while(0) +#endif // GraalPy change /* Create a new int object from a C unsigned long int */ @@ -368,6 +377,7 @@ PyLong_FromUnsignedLong(unsigned long ival) PYLONG_FROM_UINT(unsigned long, ival); } +#if 0 // GraalPy change /* Create a new int object from a C unsigned long long int. */ PyObject * diff --git a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py index 768771a9c3..2f4cda2a6d 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # The Universal Permissive License (UPL), Version 1.0 @@ -243,6 +243,35 @@ class TestPyLong(CPyExtTestCase): cmpfunc=unhandled_error_compare ) + test_PyLong_FromUnsignedLong = CPyExtFunction( + lambda args: (0, 1, max_ulong - 1), + lambda: ((),), + code=""" + PyObject* wrap_PyLong_FromUnsignedLong() { + PyObject* small = PyLong_FromUnsignedLong(0); + PyObject* one = PyLong_FromUnsignedLong(1); + PyObject* large = PyLong_FromUnsignedLong(ULONG_MAX); + PyObject* result; + if (small == NULL || one == NULL || large == NULL) { + Py_XDECREF(small); + Py_XDECREF(one); + Py_XDECREF(large); + return NULL; + } + result = PyTuple_Pack(3, small, one, large); + Py_DECREF(small); + Py_DECREF(one); + Py_DECREF(large); + return result; + } + """, + resultspec="O", + argspec='', + arguments=[], + callfunction="wrap_PyLong_FromUnsignedLong", + cmpfunc=unhandled_error_compare + ) + test_PyLong_FromSize_t = CPyExtFunction( lambda args: int(args[0]), lambda: ( diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java index 5f6a73ecc1..4c55816f07 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java @@ -51,6 +51,7 @@ import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Pointer; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyLongObject; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObject; +import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectRawPointer; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectTransfer; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Py_ssize_t; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.SIZE_T; @@ -72,6 +73,7 @@ import com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiUnaryBuiltinNode; import com.oracle.graal.python.builtins.objects.cext.capi.CExtNodes; import com.oracle.graal.python.builtins.objects.cext.capi.CExtNodes.CastToNativeLongNode; +import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.PythonToNativeNewRefNode; import com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor; import com.oracle.graal.python.builtins.objects.cext.common.CExtCommonNodes.ConvertPIntToPrimitiveNode; import com.oracle.graal.python.builtins.objects.cext.common.CExtCommonNodes.TransformPExceptionToNativeCachedNode; @@ -292,8 +294,18 @@ static long doSignedLong(long n) { } } + @CApiBuiltin(ret = PyObjectRawPointer, args = {UNSIGNED_LONG}, call = Ignored, acquireGil = false) + static long GraalPyPrivate_Long_FromUnsignedLong(long n) { + Object result = n >= 0 ? n : PFactory.createInt(PythonLanguage.get(null), convertToBigInteger(n)); + return PythonToNativeNewRefNode.executeLongUncached(result); + } + + @TruffleBoundary + private static BigInteger convertToBigInteger(long n) { + return BigInteger.valueOf(n).add(BigInteger.ONE.shiftLeft(Long.SIZE)); + } + @CApiBuiltin(name = "PyLong_FromSize_t", ret = PyObjectTransfer, args = {SIZE_T}, call = Direct) - @CApiBuiltin(name = "PyLong_FromUnsignedLong", ret = PyObjectTransfer, args = {UNSIGNED_LONG}, call = Direct) @CApiBuiltin(ret = PyObjectTransfer, args = {UNSIGNED_LONG_LONG}, call = Direct) abstract static class PyLong_FromUnsignedLongLong extends CApiUnaryBuiltinNode { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java index f0c981c1e0..76728c386b 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java @@ -340,6 +340,7 @@ public final class CApiFunction { @CApiBuiltin(name = "PyLong_FromLong", ret = PyObjectTransfer, args = {ArgDescriptor.Long}, call = CImpl) @CApiBuiltin(name = "PyLong_FromLongLong", ret = PyObjectTransfer, args = {LONG_LONG}, call = CImpl) @CApiBuiltin(name = "PyLong_FromSsize_t", ret = PyObjectTransfer, args = {Py_ssize_t}, call = CImpl) + @CApiBuiltin(name = "PyLong_FromUnsignedLong", ret = PyObjectTransfer, args = {UNSIGNED_LONG}, call = CImpl) @CApiBuiltin(name = "PyLong_FromVoidPtr", ret = PyObject, args = {Pointer}, call = CImpl) @CApiBuiltin(name = "PyMapping_Check", ret = Int, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyMapping_GetItemString", ret = PyObject, args = {PyObject, ConstCharPtrAsTruffleString}, call = CImpl) From 6c4bb913e922b204a02428ed60d92a5e220546b6 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 6 May 2026 01:14:10 +0200 Subject: [PATCH 09/17] Move PyLong_FromUnsignedLongLong to C implementation Enable the public C PyLong_FromUnsignedLongLong symbol and register PyLong_FromUnsignedLongLong as a CImpl entry point. The implementation uses the shared unsigned tagged-int fast path for small values and a private GraalPyPrivate_Long_FromUnsignedLongLong static C API helper for unsigned 64-bit values that need a multi-digit Python int. PyLong_FromSize_t remains Java-direct for its separate TODO. Rename the previous unsigned-long private fallback helper to the unsigned-long-long helper so PyLong_FromUnsignedLong also routes large values through a 64-bit unsigned fallback without truncation on platforms where unsigned long is narrower than unsigned long long. Add cpyext coverage for PyLong_FromUnsignedLongLong(0), PyLong_FromUnsignedLongLong(1), and PyLong_FromUnsignedLongLong(ULLONG_MAX). Verification: mx python-jvm passed (mxbuild/buildlog-20260506-004449.html). mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromUnsignedLong graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromUnsignedLongLong graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromSize_t passed: 3 tests, OK. JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest mx python-svm passed (mxbuild/buildlog-20260506-004955.html). JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest GRAALPYTEST_ALLOW_NO_JAVA_ASSERTIONS=true mx graalpytest --svm graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromUnsignedLong graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromUnsignedLongLong graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromSize_t passed: 3 tests, OK (mxbuild/buildlog-20260506-005318.html). Benchmarks: temporarily added graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pylong-from-unsigned-long-long.py and removed it before committing. Before each measured run, vmstat 5 13 observed the machine for about one minute; after the initial historical line the system stayed around 98-100% idle for current and 99-100% idle for baseline, and ps showed no active build/test CPU-heavy process. Command shape: --experimental-options --engine.Compilation=false graalpython/com.oracle.graal.python.benchmarks/python/harness.py -r 3 -i 9 graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pylong-from-unsigned-long-long.py , with modes 0=small, 1=ULLONG_MAX, 2=mixed. Benchmark results: mixed mode, 10,000,000 iterations: baseline median 7.548s (best 7.441s, worst 7.600s), current median 6.484s (best 6.419s, worst 6.652s), about 1.16x faster. Small-only mode, 100,000,000 iterations: baseline median 5.405s (best 5.342s, worst 5.431s), current median 0.135s (best 0.133s, worst 0.141s), about 39.96x faster. ULLONG_MAX-only mode, 10,000,000 iterations: baseline median 6.561s (best 6.509s, worst 6.674s), current median 6.413s (best 6.248s, worst 6.445s), about 1.02x faster. --- .../src/longobject.c | 4 +-- .../src/tests/cpyext/test_long.py | 31 +++++++++++++++++++ .../modules/cext/PythonCextLongBuiltins.java | 8 ++--- .../objects/cext/capi/CApiFunction.java | 1 + 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/longobject.c b/graalpython/com.oracle.graal.python.cext/src/longobject.c index 82e036981e..fd5072247b 100644 --- a/graalpython/com.oracle.graal.python.cext/src/longobject.c +++ b/graalpython/com.oracle.graal.python.cext/src/longobject.c @@ -340,7 +340,7 @@ PyLong_FromLong(long ival) if ((ival) <= INT32_MAX) { \ return int32_to_pointer((int)(ival)); \ } \ - return GraalPyPrivate_Long_FromUnsignedLong((unsigned long)(ival)); \ + return GraalPyPrivate_Long_FromUnsignedLongLong((unsigned long long)(ival)); \ } while(0) #if 0 // GraalPy change @@ -377,7 +377,6 @@ PyLong_FromUnsignedLong(unsigned long ival) PYLONG_FROM_UINT(unsigned long, ival); } -#if 0 // GraalPy change /* Create a new int object from a C unsigned long long int. */ PyObject * @@ -386,6 +385,7 @@ PyLong_FromUnsignedLongLong(unsigned long long ival) PYLONG_FROM_UINT(unsigned long long, ival); } +#if 0 // GraalPy change /* Create a new int object from a C size_t. */ PyObject * diff --git a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py index 2f4cda2a6d..71ad5deff3 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py @@ -47,6 +47,8 @@ max_long = 2 ** (long_bits - 1) - 1 min_long = -2 ** (long_bits - 1) max_ulong = 2 ** long_bits +ulonglong_bits = struct.calcsize('Q') * 8 +max_ulonglong = 2 ** ulonglong_bits ssize_t_bits = struct.calcsize('n') * 8 max_ssize_t = 2 ** (ssize_t_bits - 1) - 1 min_ssize_t = -2 ** (ssize_t_bits - 1) @@ -272,6 +274,35 @@ class TestPyLong(CPyExtTestCase): cmpfunc=unhandled_error_compare ) + test_PyLong_FromUnsignedLongLong = CPyExtFunction( + lambda args: (0, 1, max_ulonglong - 1), + lambda: ((),), + code=""" + PyObject* wrap_PyLong_FromUnsignedLongLong() { + PyObject* small = PyLong_FromUnsignedLongLong(0); + PyObject* one = PyLong_FromUnsignedLongLong(1); + PyObject* large = PyLong_FromUnsignedLongLong(ULLONG_MAX); + PyObject* result; + if (small == NULL || one == NULL || large == NULL) { + Py_XDECREF(small); + Py_XDECREF(one); + Py_XDECREF(large); + return NULL; + } + result = PyTuple_Pack(3, small, one, large); + Py_DECREF(small); + Py_DECREF(one); + Py_DECREF(large); + return result; + } + """, + resultspec="O", + argspec='', + arguments=[], + callfunction="wrap_PyLong_FromUnsignedLongLong", + cmpfunc=unhandled_error_compare + ) + test_PyLong_FromSize_t = CPyExtFunction( lambda args: int(args[0]), lambda: ( diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java index 4c55816f07..29ec449272 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java @@ -56,7 +56,6 @@ import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Py_ssize_t; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.SIZE_T; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.UNSIGNED_CHAR_PTR; -import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.UNSIGNED_LONG; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.UNSIGNED_LONG_LONG; import static com.oracle.graal.python.runtime.nativeaccess.NativeMemory.readByteArrayElements; import static com.oracle.graal.python.runtime.exception.PythonErrorType.OverflowError; @@ -294,8 +293,8 @@ static long doSignedLong(long n) { } } - @CApiBuiltin(ret = PyObjectRawPointer, args = {UNSIGNED_LONG}, call = Ignored, acquireGil = false) - static long GraalPyPrivate_Long_FromUnsignedLong(long n) { + @CApiBuiltin(ret = PyObjectRawPointer, args = {UNSIGNED_LONG_LONG}, call = Ignored, acquireGil = false) + static long GraalPyPrivate_Long_FromUnsignedLongLong(long n) { Object result = n >= 0 ? n : PFactory.createInt(PythonLanguage.get(null), convertToBigInteger(n)); return PythonToNativeNewRefNode.executeLongUncached(result); } @@ -306,8 +305,7 @@ private static BigInteger convertToBigInteger(long n) { } @CApiBuiltin(name = "PyLong_FromSize_t", ret = PyObjectTransfer, args = {SIZE_T}, call = Direct) - @CApiBuiltin(ret = PyObjectTransfer, args = {UNSIGNED_LONG_LONG}, call = Direct) - abstract static class PyLong_FromUnsignedLongLong extends CApiUnaryBuiltinNode { + abstract static class PyLong_FromSize_t extends CApiUnaryBuiltinNode { @Specialization(guards = "n >= 0") static Object doUnsignedLongPositive(long n) { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java index 76728c386b..2e5f99446e 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java @@ -341,6 +341,7 @@ public final class CApiFunction { @CApiBuiltin(name = "PyLong_FromLongLong", ret = PyObjectTransfer, args = {LONG_LONG}, call = CImpl) @CApiBuiltin(name = "PyLong_FromSsize_t", ret = PyObjectTransfer, args = {Py_ssize_t}, call = CImpl) @CApiBuiltin(name = "PyLong_FromUnsignedLong", ret = PyObjectTransfer, args = {UNSIGNED_LONG}, call = CImpl) + @CApiBuiltin(name = "PyLong_FromUnsignedLongLong", ret = PyObjectTransfer, args = {UNSIGNED_LONG_LONG}, call = CImpl) @CApiBuiltin(name = "PyLong_FromVoidPtr", ret = PyObject, args = {Pointer}, call = CImpl) @CApiBuiltin(name = "PyMapping_Check", ret = Int, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyMapping_GetItemString", ret = PyObject, args = {PyObject, ConstCharPtrAsTruffleString}, call = CImpl) From 717a405fb5d2f1fe68b31de4706b7de59a32a54b Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 6 May 2026 01:46:21 +0200 Subject: [PATCH 10/17] Move PyLong_FromSize_t to C implementation Enable the public C PyLong_FromSize_t symbol and register PyLong_FromSize_t as a CImpl entry point. Remove the Java direct PyLong_FromSize_t builtin class. The C path uses the shared unsigned tagged-int fast path and the existing GraalPyPrivate_Long_FromUnsignedLongLong fallback for size_t values requiring a multi-digit Python int. Add cpyext coverage for PyLong_FromSize_t(0), PyLong_FromSize_t(1), and PyLong_FromSize_t((size_t)-1). Verification: mx python-jvm passed (mxbuild/buildlog-20260506-011818.html). mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromUnsignedLong graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromUnsignedLongLong graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromSize_t passed: 3 tests, OK. JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest mx python-svm passed (mxbuild/buildlog-20260506-012328.html). JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest GRAALPYTEST_ALLOW_NO_JAVA_ASSERTIONS=true mx graalpytest --svm graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromUnsignedLong graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromUnsignedLongLong graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromSize_t passed: 3 tests, OK (mxbuild/buildlog-20260506-012653.html). Benchmarks: temporarily added graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pylong-from-size-t.py and removed it before committing. Before each measured run, vmstat 5 13 observed the machine for about one minute; after the initial historical line the system stayed around 99-100% idle for current and baseline, and ps showed no active build/test CPU-heavy process. Command shape: --experimental-options --engine.Compilation=false graalpython/com.oracle.graal.python.benchmarks/python/harness.py -r 3 -i 9 graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pylong-from-size-t.py , with modes 0=small, 1=(size_t)-1, 2=mixed. Benchmark results: mixed mode, 10,000,000 iterations: baseline median 7.259s (best 7.164s, worst 7.455s), current median 6.316s (best 6.263s, worst 6.437s), about 1.15x faster. Small-only mode, 100,000,000 iterations: baseline median 5.293s (best 5.152s, worst 5.361s), current median 0.133s (best 0.131s, worst 0.138s), about 39.80x faster. (size_t)-1-only mode, 10,000,000 iterations: baseline median 6.909s (best 6.835s, worst 6.930s), current median 6.286s (best 6.176s, worst 6.346s), about 1.10x faster. --- .../src/longobject.c | 2 +- .../src/tests/cpyext/test_long.py | 34 ++++++++++++++----- .../modules/cext/PythonCextLongBuiltins.java | 20 ----------- .../objects/cext/capi/CApiFunction.java | 1 + 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/longobject.c b/graalpython/com.oracle.graal.python.cext/src/longobject.c index fd5072247b..cd0cd98b4d 100644 --- a/graalpython/com.oracle.graal.python.cext/src/longobject.c +++ b/graalpython/com.oracle.graal.python.cext/src/longobject.c @@ -385,7 +385,6 @@ PyLong_FromUnsignedLongLong(unsigned long long ival) PYLONG_FROM_UINT(unsigned long long, ival); } -#if 0 // GraalPy change /* Create a new int object from a C size_t. */ PyObject * @@ -394,6 +393,7 @@ PyLong_FromSize_t(size_t ival) PYLONG_FROM_UINT(size_t, ival); } +#if 0 // GraalPy change /* Create a new int object from a C double */ PyObject * diff --git a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py index 71ad5deff3..76dad631a3 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py @@ -49,6 +49,8 @@ max_ulong = 2 ** long_bits ulonglong_bits = struct.calcsize('Q') * 8 max_ulonglong = 2 ** ulonglong_bits +size_t_bits = struct.calcsize('P') * 8 +max_size_t = 2 ** size_t_bits ssize_t_bits = struct.calcsize('n') * 8 max_ssize_t = 2 ** (ssize_t_bits - 1) - 1 min_ssize_t = -2 ** (ssize_t_bits - 1) @@ -304,15 +306,31 @@ class TestPyLong(CPyExtTestCase): ) test_PyLong_FromSize_t = CPyExtFunction( - lambda args: int(args[0]), - lambda: ( - (0,), - (1,), - (0xffffffff,), - ), + lambda args: (0, 1, max_size_t - 1), + lambda: ((),), + code=""" + PyObject* wrap_PyLong_FromSize_t() { + PyObject* small = PyLong_FromSize_t(0); + PyObject* one = PyLong_FromSize_t(1); + PyObject* large = PyLong_FromSize_t((size_t)-1); + PyObject* result; + if (small == NULL || one == NULL || large == NULL) { + Py_XDECREF(small); + Py_XDECREF(one); + Py_XDECREF(large); + return NULL; + } + result = PyTuple_Pack(3, small, one, large); + Py_DECREF(small); + Py_DECREF(one); + Py_DECREF(large); + return result; + } + """, resultspec="O", - argspec='n', - arguments=["size_t n"], + argspec='', + arguments=[], + callfunction="wrap_PyLong_FromSize_t", cmpfunc=unhandled_error_compare ) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java index 29ec449272..e2e116c747 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java @@ -304,26 +304,6 @@ private static BigInteger convertToBigInteger(long n) { return BigInteger.valueOf(n).add(BigInteger.ONE.shiftLeft(Long.SIZE)); } - @CApiBuiltin(name = "PyLong_FromSize_t", ret = PyObjectTransfer, args = {SIZE_T}, call = Direct) - abstract static class PyLong_FromSize_t extends CApiUnaryBuiltinNode { - - @Specialization(guards = "n >= 0") - static Object doUnsignedLongPositive(long n) { - return n; - } - - @Specialization(guards = "n < 0") - static Object doUnsignedLongNegative(long n, - @Bind PythonLanguage language) { - return PFactory.createInt(language, convertToBigInteger(n)); - } - - @TruffleBoundary - private static BigInteger convertToBigInteger(long n) { - return BigInteger.valueOf(n).add(BigInteger.ONE.shiftLeft(Long.SIZE)); - } - } - @CApiBuiltin(ret = Pointer, args = {PyObject}, call = Direct) public abstract static class PyLong_AsVoidPtr extends CApiUnaryBuiltinNode { @Child private ConvertPIntToPrimitiveNode asPrimitiveNode; diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java index 2e5f99446e..824ae36006 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java @@ -339,6 +339,7 @@ public final class CApiFunction { @CApiBuiltin(name = "PyLong_FromString", ret = PyObject, args = {ConstCharPtrAsTruffleString, CHAR_PTR_LIST, Int}, call = CImpl) @CApiBuiltin(name = "PyLong_FromLong", ret = PyObjectTransfer, args = {ArgDescriptor.Long}, call = CImpl) @CApiBuiltin(name = "PyLong_FromLongLong", ret = PyObjectTransfer, args = {LONG_LONG}, call = CImpl) + @CApiBuiltin(name = "PyLong_FromSize_t", ret = PyObjectTransfer, args = {SIZE_T}, call = CImpl) @CApiBuiltin(name = "PyLong_FromSsize_t", ret = PyObjectTransfer, args = {Py_ssize_t}, call = CImpl) @CApiBuiltin(name = "PyLong_FromUnsignedLong", ret = PyObjectTransfer, args = {UNSIGNED_LONG}, call = CImpl) @CApiBuiltin(name = "PyLong_FromUnsignedLongLong", ret = PyObjectTransfer, args = {UNSIGNED_LONG_LONG}, call = CImpl) From afacbaa387d16ef1b340cd657a49772d860e627a Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 6 May 2026 02:12:43 +0200 Subject: [PATCH 11/17] Move PyLong_FromDouble to C implementation Enable the public C PyLong_FromDouble symbol and register it as CImpl. The C path handles finite double values fitting in long by calling PyLong_FromLong((long)dval), and falls back to a private static Java helper for NaN, infinities, and large finite values because the CPython multi-digit path uses _PyLong_New, which is not available through this route. Add cpyext coverage for PyLong_FromDouble(1.0e100) to exercise the large finite fallback path. Verification: mx python-jvm passed (mxbuild/buildlog-20260506-014930.html). mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromDouble passed: 1 test, OK. JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest mx python-svm passed (mxbuild/buildlog-20260506-015441.html). JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest GRAALPYTEST_ALLOW_NO_JAVA_ASSERTIONS=true mx graalpytest --svm graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromDouble passed: 1 test, OK (mxbuild/buildlog-20260506-015807.html). git diff --check passed. Temporarily added graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pylong-from-double.py covering integral, non-integral, large finite fallback, and mixed loops; removed the temporary benchmark before committing. Quietness checks before benchmark: vmstat 5 13 over about one minute showed 99-100% idle after the initial historical line; ps showed no active build/test CPU-heavy process. After stopping an oversized exploratory large-fallback run, repeated vmstat 5 13 showed 99-100% idle again before the final benchmark runs. Emacs appeared with a lifetime CPU average, but vmstat showed no active CPU pressure during the observation windows. Benchmark command shape: --experimental-options --engine.Compilation=false graalpython/com.oracle.graal.python.benchmarks/python/harness.py -r 3 -i 9 graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pylong-from-double.py . Modes: 0 = integral 42.0, 1 = non-integral 42.75, 2 = large finite 1.0e100, 3 = mixed. Current build used /tmp/graalpy-pylong-from-double-current-20260506-0200/bin/graalpy; clean baseline used /home/tim/dev/graalpython/.agent-shell/worktrees/gr-49498/graalpython-pylong-double-baseline/mxbuild/linux-amd64/GRAALPY_NATIVE_STANDALONE/bin/graalpy built from HEAD before this item (mxbuild/buildlog-20260506-020317.html). Benchmark results: Integral, 100,000,000 iterations, mode 0: baseline median 5.439s (best 5.344s, worst 5.584s); current median 0.213s (best 0.210s, worst 0.218s), about 25.51x faster. Non-integral, 100,000,000 iterations, mode 1: baseline median 5.498s (best 5.430s, worst 5.630s); current median 0.213s (best 0.209s, worst 0.223s), about 25.83x faster. Large finite fallback, 100,000 iterations, mode 2: baseline median 0.069s (best 0.066s, worst 0.075s); current median 0.066s (best 0.053s, worst 0.071s), about 1.03x faster. Mixed, 100,000 iterations, mode 3: baseline median 0.081s (best 0.073s, worst 0.090s); current median 0.070s (best 0.057s, worst 0.072s), about 1.16x faster. Conclusion: moving PyLong_FromDouble to CImpl with a C fast path for values fitting in long improves the native no-compilation benchmark while preserving the Java fallback behavior for large finite double values. --- .../com.oracle.graal.python.cext/src/longobject.c | 5 +++-- .../src/tests/cpyext/test_long.py | 1 + .../modules/cext/PythonCextLongBuiltins.java | 13 ++++--------- .../builtins/objects/cext/capi/CApiFunction.java | 1 + 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/longobject.c b/graalpython/com.oracle.graal.python.cext/src/longobject.c index cd0cd98b4d..2b6d3ade03 100644 --- a/graalpython/com.oracle.graal.python.cext/src/longobject.c +++ b/graalpython/com.oracle.graal.python.cext/src/longobject.c @@ -393,7 +393,6 @@ PyLong_FromSize_t(size_t ival) PYLONG_FROM_UINT(size_t, ival); } -#if 0 // GraalPy change /* Create a new int object from a C double */ PyObject * @@ -414,6 +413,7 @@ PyLong_FromDouble(double dval) return PyLong_FromLong((long)dval); } +#if 0 // GraalPy change PyLongObject *v; double frac; int i, ndig, expo, neg; @@ -449,8 +449,9 @@ PyLong_FromDouble(double dval) _PyLong_FlipSign(v); } return (PyObject *)v; -} #endif // GraalPy change + return GraalPyPrivate_Long_FromDouble(dval); +} /* Checking for overflow in PyLong_AsLong is a PITA since C doesn't define * anything about what happens when a signed integer operation overflows, diff --git a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py index 76dad631a3..ba161f05e5 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py @@ -340,6 +340,7 @@ class TestPyLong(CPyExtTestCase): (0.0,), (-1.0,), (-11.123456789123456789,), + (1.0e100,), ), resultspec="O", argspec='d', diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java index e2e116c747..8bb83e0f7e 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java @@ -208,15 +208,10 @@ static long getDC(Object n, } } - @CApiBuiltin(ret = PyObjectTransfer, args = {ArgDescriptor.Double}, call = Direct) - abstract static class PyLong_FromDouble extends CApiUnaryBuiltinNode { - - @Specialization - static Object fromDouble(double d, - @Bind Node inliningTarget, - @Cached PyLongFromDoubleNode pyLongFromDoubleNode) { - return pyLongFromDoubleNode.execute(inliningTarget, d); - } + @CApiBuiltin(ret = PyObjectRawPointer, args = {ArgDescriptor.Double}, call = Ignored, acquireGil = false) + static long GraalPyPrivate_Long_FromDouble(double d) { + Object result = PyLongFromDoubleNode.executeUncached(d); + return PythonToNativeNewRefNode.executeLongUncached(result); } @CApiBuiltin(ret = PyObjectTransfer, args = {ConstCharPtrAsTruffleString, Int}, call = Ignored) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java index 824ae36006..b8e7dd65aa 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java @@ -337,6 +337,7 @@ public final class CApiFunction { @CApiBuiltin(name = "PyLong_AsUnsignedLongLongMask", ret = UNSIGNED_LONG_LONG, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyLong_AsUnsignedLongMask", ret = UNSIGNED_LONG, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyLong_FromString", ret = PyObject, args = {ConstCharPtrAsTruffleString, CHAR_PTR_LIST, Int}, call = CImpl) + @CApiBuiltin(name = "PyLong_FromDouble", ret = PyObjectTransfer, args = {ArgDescriptor.Double}, call = CImpl) @CApiBuiltin(name = "PyLong_FromLong", ret = PyObjectTransfer, args = {ArgDescriptor.Long}, call = CImpl) @CApiBuiltin(name = "PyLong_FromLongLong", ret = PyObjectTransfer, args = {LONG_LONG}, call = CImpl) @CApiBuiltin(name = "PyLong_FromSize_t", ret = PyObjectTransfer, args = {SIZE_T}, call = CImpl) From 5207cff4026757b4c6ec14537e25e3c50f9b3176 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 6 May 2026 02:52:05 +0200 Subject: [PATCH 12/17] Move _PyLong_Sign to C implementation Enable the public C _PyLong_Sign symbol and register it as CImpl. Remove the Java direct _PyLong_Sign builtin class. The C implementation handles GraalPy tagged-int handles directly and computes non-tagged long signs from a single GraalPyPrivate_Long_lv_tag read. Verification: mx python-jvm passed (mxbuild/buildlog-20260506-024013.html). mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test__PyLong_Sign passed: 1 test, OK. JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest mx python-svm passed (mxbuild/buildlog-20260506-024520.html). JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest GRAALPYTEST_ALLOW_NO_JAVA_ASSERTIONS=true mx graalpytest --svm graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test__PyLong_Sign passed: 1 test, OK (mxbuild/buildlog-20260506-024844.html). git diff --check passed. Temporarily added graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pylong-sign.py covering negative, zero, positive, large multi-digit, and mixed loops; removed the temporary benchmark before committing. Quietness checks before benchmark: vmstat 5 13 over about one minute showed 99-100% idle after the initial historical line; ps showed no active build/test CPU-heavy process. After stopping an oversized exploratory large-mode run, repeated vmstat 5 13 showed 99-100% idle again. After the final native rebuild and focused SVM test, a final vmstat 5 13 again showed 99-100% idle before current measurements. Emacs appeared with a lifetime CPU average, but vmstat showed no active CPU pressure during the observation windows. Benchmark command shape: --experimental-options --engine.Compilation=false graalpython/com.oracle.graal.python.benchmarks/python/harness.py -r 3 -i 9 graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pylong-sign.py . Modes: 0 = negative, 1 = zero, 2 = positive, 3 = large multi-digit, 4 = mixed negative + zero + positive + large. Current build used /tmp/graalpy-pylong-sign-current-20260506-0248/bin/graalpy; clean baseline used /home/tim/dev/graalpython/.agent-shell/worktrees/gr-49498/graalpython-pylong-sign-baseline/mxbuild/linux-amd64/GRAALPY_NATIVE_STANDALONE/bin/graalpy built from HEAD before this item (mxbuild/buildlog-20260506-022927.html). Benchmark results: Negative tagged int, 100,000,000 iterations, mode 0: baseline median 4.508s (best 4.436s, worst 4.553s); current median 0.140s (best 0.139s, worst 0.147s), about 32.11x faster. Zero tagged int, 100,000,000 iterations, mode 1: baseline median 4.347s (best 4.263s, worst 4.393s); current median 0.140s (best 0.139s, worst 0.143s), about 31.01x faster. Positive tagged int, 100,000,000 iterations, mode 2: baseline median 4.439s (best 4.398s, worst 4.530s); current median 0.142s (best 0.141s, worst 0.146s), about 31.22x faster. Large multi-digit int, 1,000,000 iterations, mode 3: baseline median 0.045s (best 0.044s, worst 0.050s); current median 0.051s (best 0.049s, worst 0.054s), about 0.89x as fast. Mixed, 1,000,000 iterations, mode 4: baseline median 0.176s (best 0.174s, worst 0.180s); current median 0.058s (best 0.056s, worst 0.061s), about 3.01x faster. Conclusion: moving _PyLong_Sign to CImpl with a tagged-int fast path improves the common tagged-int and mixed native no-compilation benchmarks substantially. The isolated large multi-digit path is slightly slower than the Java direct baseline, but the mixed benchmark still improves because the tagged-int cases dominate this low-level API's common use. --- .../src/longobject.c | 11 ++- .../modules/cext/PythonCextLongBuiltins.java | 91 ------------------- .../objects/cext/capi/CApiFunction.java | 1 + 3 files changed, 10 insertions(+), 93 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/longobject.c b/graalpython/com.oracle.graal.python.cext/src/longobject.c index 2b6d3ade03..75a91632a4 100644 --- a/graalpython/com.oracle.graal.python.cext/src/longobject.c +++ b/graalpython/com.oracle.graal.python.cext/src/longobject.c @@ -626,20 +626,27 @@ PyLong_AsUnsignedLongMask(PyObject *op) return (unsigned long) GraalPyPrivate_Long_AsPrimitive(op, MODE_COERCE_MASK, sizeof(unsigned long)); } -#if 0 // GraalPy change int _PyLong_Sign(PyObject *vv) { + assert(vv != NULL); + if (points_to_py_int_handle(vv)) { + int64_t value = pointer_to_int64(vv); + return (value > 0) - (value < 0); + } + PyLongObject *v = (PyLongObject *)vv; assert(v != NULL); assert(PyLong_Check(v)); +#if 0 // GraalPy change if (_PyLong_IsCompact(v)) { return _PyLong_CompactSign(v); } return _PyLong_NonCompactSign(v); -} #endif // GraalPy change + return 1 - (GraalPyPrivate_Long_lv_tag(v) & SIGN_MASK); +} static int bit_length_digit(digit x) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java index 8bb83e0f7e..5b22ed230a 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java @@ -106,97 +106,6 @@ public final class PythonCextLongBuiltins { - @CApiBuiltin(ret = Int, args = {PyObject}, call = Direct) - abstract static class _PyLong_Sign extends CApiUnaryBuiltinNode { - - @SuppressWarnings("unused") - @Specialization(guards = "n == 0") - static int sign(int n) { - return 0; - } - - @SuppressWarnings("unused") - @Specialization(guards = "n < 0") - static int signNeg(int n) { - return -1; - } - - @SuppressWarnings("unused") - @Specialization(guards = "n > 0") - static int signPos(int n) { - return 1; - } - - @SuppressWarnings("unused") - @Specialization(guards = "n == 0") - static int sign(long n) { - return 0; - } - - @SuppressWarnings("unused") - @Specialization(guards = "n < 0") - static int signNeg(long n) { - return -1; - } - - @SuppressWarnings("unused") - @Specialization(guards = "n > 0") - static int signPos(long n) { - return 1; - } - - @SuppressWarnings("unused") - @Specialization(guards = "b") - static int signTrue(boolean b) { - return 1; - } - - @SuppressWarnings("unused") - @Specialization(guards = "!b") - static int signFalse(boolean b) { - return 0; - } - - @Specialization - static int sign(PInt n, - @Bind Node inliningTarget, - @Cached InlinedBranchProfile zeroProfile, - @Cached InlinedBranchProfile negProfile) { - if (n.isNegative()) { - negProfile.enter(inliningTarget); - return -1; - } else if (n.isZero()) { - zeroProfile.enter(inliningTarget); - return 0; - } else { - return 1; - } - } - - @SuppressWarnings("unused") - @Specialization(guards = {"!canBeInteger(obj)", "isPIntSubtype(inliningTarget, obj, getClassNode, isSubtypeNode)"}) - static Object signNative(Object obj, - @Bind Node inliningTarget, - @Shared @Cached GetClassNode getClassNode, - @Shared @Cached IsSubtypeNode isSubtypeNode) { - // function returns int, but -1 is expected result for 'n < 0' - throw CompilerDirectives.shouldNotReachHere("not yet implemented"); - } - - @Specialization(guards = {"!isInteger(obj)", "!isPInt(obj)", "!isPIntSubtype(inliningTarget, obj,getClassNode,isSubtypeNode)"}) - static Object sign(@SuppressWarnings("unused") Object obj, - @Bind Node inliningTarget, - @SuppressWarnings("unused") @Shared @Cached GetClassNode getClassNode, - @SuppressWarnings("unused") @Shared @Cached IsSubtypeNode isSubtypeNode) { - // assert(PyLong_Check(v)); - throw CompilerDirectives.shouldNotReachHere(); - } - - protected boolean isPIntSubtype(Node inliningTarget, Object obj, GetClassNode getClassNode, IsSubtypeNode isSubtypeNode) { - return isSubtypeNode.execute(getClassNode.execute(inliningTarget, obj), PythonBuiltinClassType.PInt); - } - } - @CApiBuiltin(ret = Py_ssize_t, args = {PyLongObject}, call = Ignored) abstract static class GraalPyPrivate_Long_DigitCount extends CApiUnaryBuiltinNode { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java index b8e7dd65aa..bd7a8358a4 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java @@ -336,6 +336,7 @@ public final class CApiFunction { @CApiBuiltin(name = "PyLong_AsUnsignedLongLong", ret = UNSIGNED_LONG_LONG, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyLong_AsUnsignedLongLongMask", ret = UNSIGNED_LONG_LONG, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyLong_AsUnsignedLongMask", ret = UNSIGNED_LONG, args = {PyObject}, call = CImpl) + @CApiBuiltin(name = "_PyLong_Sign", ret = Int, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyLong_FromString", ret = PyObject, args = {ConstCharPtrAsTruffleString, CHAR_PTR_LIST, Int}, call = CImpl) @CApiBuiltin(name = "PyLong_FromDouble", ret = PyObjectTransfer, args = {ArgDescriptor.Double}, call = CImpl) @CApiBuiltin(name = "PyLong_FromLong", ret = PyObjectTransfer, args = {ArgDescriptor.Long}, call = CImpl) From 74c5b806df77f2b9be22aa94f12e7044a4961271 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 6 May 2026 03:17:29 +0200 Subject: [PATCH 13/17] Move _PyLong_NumBits to C implementation Enable the public C _PyLong_NumBits symbol and register it as CImpl. Remove the Java direct _PyLong_NumBits builtin class. The C implementation handles GraalPy tagged-int handles directly and falls back to a private static Java helper for non-tagged longs; the pure CPython digit-array path was unsafe for managed GraalPy ints and crashed in the focused JVM cpyext test. Verification: mx python-jvm passed (mxbuild/buildlog-20260506-025834.html). mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test__PyLong_NumBits passed: 1 test, OK. JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest mx python-svm passed (mxbuild/buildlog-20260506-030344.html). JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest GRAALPYTEST_ALLOW_NO_JAVA_ASSERTIONS=true mx graalpytest --svm graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test__PyLong_NumBits passed: 1 test, OK (mxbuild/buildlog-20260506-030707.html). git diff --check passed. Temporarily added graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pylong-numbits.py covering small tagged int, large multi-digit int, negative int, and mixed loops; removed the temporary benchmark before committing. Quietness check before benchmark: vmstat 5 13 over about one minute showed 99-100% idle after the initial historical line; ps showed no active build/test CPU-heavy process. Emacs appeared with a lifetime CPU average, but vmstat showed no active CPU pressure during the observation window. Benchmark command shape: --experimental-options --engine.Compilation=false graalpython/com.oracle.graal.python.benchmarks/python/harness.py -r 3 -i 9 graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pylong-numbits.py . Modes: 0 = small tagged int, 1 = large multi-digit int, 2 = negative int, 3 = mixed small + large + negative. Current build used /tmp/graalpy-pylong-numbits-current-20260506-0307/bin/graalpy; clean baseline used /home/tim/dev/graalpython/.agent-shell/worktrees/gr-49498/graalpython-pylong-numbits-baseline/mxbuild/linux-amd64/GRAALPY_NATIVE_STANDALONE/bin/graalpy built from HEAD before this item (mxbuild/buildlog-20260506-031214.html). Benchmark results: Small tagged int, 100,000,000 iterations, mode 0: baseline median 4.598s (best 4.539s, worst 4.698s); current median 0.142s (best 0.141s, worst 0.152s), about 32.44x faster. Large multi-digit int, 1,000,000 iterations, mode 1: baseline median 0.050s (best 0.048s, worst 0.054s); current median 0.020s (best 0.020s, worst 0.021s), about 2.48x faster. Negative int, 1,000,000 iterations, mode 2: baseline median 0.054s (best 0.051s, worst 0.059s); current median 0.020s (best 0.020s, worst 0.021s), about 2.65x faster. Mixed, 1,000,000 iterations, mode 3: baseline median 0.197s (best 0.195s, worst 0.202s); current median 0.042s (best 0.041s, worst 0.044s), about 4.69x faster. Conclusion: moving _PyLong_NumBits to CImpl with a tagged-int C fast path and Java fallback for non-tagged longs improves all measured native no-compilation benchmark modes while preserving managed-int safety. --- .../src/longobject.c | 13 +++++++++- .../modules/cext/PythonCextLongBuiltins.java | 24 ++++++++++++------- .../objects/cext/capi/CApiFunction.java | 1 + 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/longobject.c b/graalpython/com.oracle.graal.python.cext/src/longobject.c index 75a91632a4..5c248a481d 100644 --- a/graalpython/com.oracle.graal.python.cext/src/longobject.c +++ b/graalpython/com.oracle.graal.python.cext/src/longobject.c @@ -658,10 +658,19 @@ bit_length_digit(digit x) return _Py_bit_length((unsigned long)x); } -#if 0 // GraalPy change size_t _PyLong_NumBits(PyObject *vv) { + assert(vv != NULL); + if (points_to_py_int_handle(vv)) { + int64_t value = pointer_to_int64(vv); + unsigned long magnitude = value < 0 ? (unsigned long)-value : (unsigned long)value; + return (size_t)_Py_bit_length(magnitude); + } + + return GraalPyPrivate_Long_NumBits(vv); + +#if 0 // GraalPy change PyLongObject *v = (PyLongObject *)vv; size_t result = 0; Py_ssize_t ndigits; @@ -687,8 +696,10 @@ _PyLong_NumBits(PyObject *vv) PyErr_SetString(PyExc_OverflowError, "int has too many bits " "to express in a platform size_t"); return (size_t)-1; +#endif // GraalPy change } +#if 0 // GraalPy change PyObject * _PyLong_FromByteArray(const unsigned char* bytes, size_t n, int little_endian, int is_signed) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java index 5b22ed230a..e03765cf8e 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java @@ -72,6 +72,7 @@ import com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiUnaryBuiltinNode; import com.oracle.graal.python.builtins.objects.cext.capi.CExtNodes; import com.oracle.graal.python.builtins.objects.cext.capi.CExtNodes.CastToNativeLongNode; +import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.NativeToPythonNode; import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.PythonToNativeNewRefNode; import com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor; import com.oracle.graal.python.builtins.objects.cext.common.CExtCommonNodes.ConvertPIntToPrimitiveNode; @@ -208,6 +209,21 @@ private static BigInteger convertToBigInteger(long n) { return BigInteger.valueOf(n).add(BigInteger.ONE.shiftLeft(Long.SIZE)); } + @CApiBuiltin(ret = SIZE_T, args = {PyObjectRawPointer}, call = Ignored, acquireGil = false) + static long GraalPyPrivate_Long_NumBits(long objPtr) { + Object obj = NativeToPythonNode.executeRawUncached(objPtr); + if (obj instanceof Integer value) { + return Integer.SIZE - Integer.numberOfLeadingZeros(Math.abs(value)); + } else if (obj instanceof Long value) { + return Long.SIZE - Long.numberOfLeadingZeros(Math.abs(value)); + } else if (obj instanceof PInt value) { + return value.bitLength(); + } else if (obj instanceof Boolean value) { + return value ? 1 : 0; + } + throw CompilerDirectives.shouldNotReachHere(); + } + @CApiBuiltin(ret = Pointer, args = {PyObject}, call = Direct) public abstract static class PyLong_AsVoidPtr extends CApiUnaryBuiltinNode { @Child private ConvertPIntToPrimitiveNode asPrimitiveNode; @@ -339,12 +355,4 @@ static Object convert(long charPtr, long size, int littleEndian, int signed, } } - @CApiBuiltin(ret = SIZE_T, args = {PyObject}, call = Direct) - abstract static class _PyLong_NumBits extends CApiUnaryBuiltinNode { - @Specialization - static long numBits(Object obj, - @Cached IntBuiltins.BitLengthNode bitLengthNode) { - return bitLengthNode.execute(obj); - } - } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java index bd7a8358a4..60dddfe29d 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java @@ -336,6 +336,7 @@ public final class CApiFunction { @CApiBuiltin(name = "PyLong_AsUnsignedLongLong", ret = UNSIGNED_LONG_LONG, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyLong_AsUnsignedLongLongMask", ret = UNSIGNED_LONG_LONG, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyLong_AsUnsignedLongMask", ret = UNSIGNED_LONG, args = {PyObject}, call = CImpl) + @CApiBuiltin(name = "_PyLong_NumBits", ret = SIZE_T, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "_PyLong_Sign", ret = Int, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyLong_FromString", ret = PyObject, args = {ConstCharPtrAsTruffleString, CHAR_PTR_LIST, Int}, call = CImpl) @CApiBuiltin(name = "PyLong_FromDouble", ret = PyObjectTransfer, args = {ArgDescriptor.Double}, call = CImpl) From 1ebdfb6f2eefe63ddcb93c1e5867c47b4b80e812 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 6 May 2026 04:12:21 +0200 Subject: [PATCH 14/17] Move PyLong_AsVoidPtr to C implementation Move PyLong_AsVoidPtr from the Java-direct C API builtin to a CImpl entry in CApiFunction. Enable the public C symbol in longobject.c with a tagged-int C fast path for common small positive and negative handles, and keep non-tagged behavior on the private Java fallback GraalPyPrivate_Long_AsVoidPtr. The fallback preserves unsigned pointer-sized positive values such as UINTPTR_MAX while still raising overflow beyond the pointer range. Add cpyext coverage for PyLong_AsVoidPtr round-tripping 0, 42, -1, 0xffffffff, 0xffffffffffffffff, and 0x10000000000000000. Verification: mx python-jvm passed (mxbuild/buildlog-20260506-034842.html). Focused JVM tests passed: mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_AsVoidPtr graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_AsVoidPtrAllocated graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyLong_FromAndToVoidPtrAllocated, 3 tests OK. JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest mx python-svm passed (mxbuild/buildlog-20260506-035346.html; native focused-test rebuild mxbuild/buildlog-20260506-035709.html). Focused native tests passed with GRAALPYTEST_ALLOW_NO_JAVA_ASSERTIONS=true, 3 tests OK. mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/ passed: 2811 tests in 700.20s, OK (passed=2792, skipped=19). git diff --check passed. Temporary benchmark c-pylong-asvoidptr.py was used and removed before commit. Benchmarks used --experimental-options --engine.Compilation=false graalpython/com.oracle.graal.python.benchmarks/python/harness.py -r 3 -i 9 graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pylong-asvoidptr.py . Modes: 0 small positive tagged int; 1 signed pointer-sized positive value, UINTPTR_MAX >> 1; 2 negative tagged int; 3 overflow beyond pointer size; 4 mixed small + pointer-sized + negative. Before benchmarking, vmstat 5 13 showed 99-100% idle after the initial historical sample and ps showed no active build/test CPU-heavy process. Benchmark results: mode 0, 100000000 iterations: baseline median 4.489s (best 4.446s, worst 4.568s), current median 0.126s (best 0.121s, worst 0.128s), about 35.64x faster. Mode 1, 10000000 iterations: baseline median 0.766s (best 0.757s, worst 0.781s), current median 0.764s (best 0.740s, worst 0.800s), effectively flat/slightly faster. Mode 2, 100000000 iterations: baseline median 4.362s (best 4.290s, worst 4.503s), current median 0.127s (best 0.125s, worst 0.133s), about 34.42x faster. Mode 3, 1000000 iterations: baseline median 0.810s (best 0.780s, worst 0.824s), current median 0.839s (best 0.820s, worst 0.853s), about 0.97x as fast. Mode 4, 10000000 iterations: baseline median 1.690s (best 1.666s, worst 1.716s), current median 0.833s (best 0.816s, worst 0.849s), about 2.03x faster. Conclusion: moving PyLong_AsVoidPtr to CImpl with a tagged-int C fast path and Java fallback for non-tagged values improves common tagged-int and mixed native no-compilation workloads while preserving pointer-sized unsigned correctness. The isolated overflow path is slightly slower, but the mixed benchmark improves substantially and the large pointer-sized path remains baseline-level. --- .../src/longobject.c | 12 ++++++- .../src/tests/cpyext/test_long.py | 34 +++++++++++++++++++ .../modules/cext/PythonCextLongBuiltins.java | 26 +++++++++----- .../objects/cext/capi/CApiFunction.java | 1 + 4 files changed, 63 insertions(+), 10 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/longobject.c b/graalpython/com.oracle.graal.python.cext/src/longobject.c index 5c248a481d..6aa2bb28ad 100644 --- a/graalpython/com.oracle.graal.python.cext/src/longobject.c +++ b/graalpython/com.oracle.graal.python.cext/src/longobject.c @@ -958,9 +958,9 @@ PyLong_FromVoidPtr(void *p) return PyLong_FromUnsignedLongLong((uint64_t)p); } -#if 0 // GraalPy change /* Get a C pointer from an int object. */ +#if 0 // GraalPy change void * PyLong_AsVoidPtr(PyObject *vv) { @@ -993,6 +993,16 @@ PyLong_AsVoidPtr(PyObject *vv) return NULL; return (void *)x; } +#else +void * +PyLong_AsVoidPtr(PyObject *vv) +{ + if (points_to_py_int_handle(vv)) { + return (void *)(uintptr_t)pointer_to_int64(vv); + } + + return (void *)GraalPyPrivate_Long_AsVoidPtr(vv); +} #endif // GraalPy change /* Initial long long support by Chris Herborth (chrish@qnx.com), later diff --git a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py index ba161f05e5..0686ab33cb 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py @@ -116,6 +116,15 @@ def _reference_fromvoidptr(args): return n +def _reference_asvoidptr_roundtrip(args): + n = args[0] + if n < min_long or n >= max_ulonglong: + raise OverflowError("Python int too large to convert") + if n < 0: + return _reference_fromvoidptr(args) + return n + + def _reference_fromlong(args): n = args[0] return n @@ -363,6 +372,31 @@ class TestPyLong(CPyExtTestCase): cmpfunc=unhandled_error_compare ) + test_PyLong_AsVoidPtr = CPyExtFunction( + _reference_asvoidptr_roundtrip, + lambda: ( + (0,), + (42,), + (-1,), + (0xffffffff,), + (0xffffffffffffffff,), + (0x10000000000000000,), + ), + code="""PyObject* wrap_PyLong_AsVoidPtr(PyObject* obj) { + void* ptr = PyLong_AsVoidPtr(obj); + if (ptr == NULL && PyErr_Occurred()) { + return NULL; + } + return PyLong_FromVoidPtr(ptr); + } + """, + resultspec="O", + argspec='O', + arguments=["PyObject* obj"], + callfunction="wrap_PyLong_AsVoidPtr", + cmpfunc=unhandled_error_compare + ) + test_PyLong_FromVoidPtrAllocated = CPyExtFunction( lambda args: int, lambda: ((None,),), diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java index e03765cf8e..5a24fc4fba 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java @@ -224,8 +224,8 @@ static long GraalPyPrivate_Long_NumBits(long objPtr) { throw CompilerDirectives.shouldNotReachHere(); } - @CApiBuiltin(ret = Pointer, args = {PyObject}, call = Direct) - public abstract static class PyLong_AsVoidPtr extends CApiUnaryBuiltinNode { + @CApiBuiltin(ret = Pointer, args = {PyObject}, call = Ignored) + abstract static class GraalPyPrivate_Long_AsVoidPtr extends CApiUnaryBuiltinNode { @Child private ConvertPIntToPrimitiveNode asPrimitiveNode; @Child private TransformPExceptionToNativeCachedNode transformExceptionToNativeNode; @@ -247,13 +247,12 @@ long doPointer(PInt n, try { return n.longValueExact(); } catch (OverflowException e) { - overflowProfile.enter(inliningTarget); - try { - throw raiseNode.raise(inliningTarget, OverflowError, ErrorMessages.PYTHON_INT_TOO_LARGE_TO_CONV_TO, "C long"); - } catch (PException pe) { - ensureTransformExcNode().execute(pe); - return 0; + if (!n.isNegative() && n.bitLength() <= Long.SIZE) { + return n.longValue(); } + overflowProfile.enter(inliningTarget); + transformOverflow(inliningTarget, raiseNode); + return 0; } } @@ -269,7 +268,8 @@ long doGeneric(Object n, try { return asPrimitiveNode.executeLongCached(n, 0, Long.BYTES); } catch (UnexpectedResultException e) { - throw raiseNode.raise(inliningTarget, OverflowError, ErrorMessages.PYTHON_INT_TOO_LARGE_TO_CONV_TO, "C long"); + transformOverflow(inliningTarget, raiseNode); + return 0; } } catch (PException e) { ensureTransformExcNode().execute(e); @@ -277,6 +277,14 @@ long doGeneric(Object n, } } + private void transformOverflow(Node inliningTarget, PRaiseNode raiseNode) { + try { + throw raiseNode.raise(inliningTarget, OverflowError, ErrorMessages.PYTHON_INT_TOO_LARGE_TO_CONV_TO, "C long"); + } catch (PException pe) { + ensureTransformExcNode().execute(pe); + } + } + private TransformPExceptionToNativeCachedNode ensureTransformExcNode() { if (transformExceptionToNativeNode == null) { CompilerDirectives.transferToInterpreterAndInvalidate(); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java index 60dddfe29d..b4c771bdf5 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java @@ -332,6 +332,7 @@ public final class CApiFunction { @CApiBuiltin(name = "PyLong_AsLongLongAndOverflow", ret = LONG_LONG, args = {PyObject, INT_LIST}, call = CImpl) @CApiBuiltin(name = "PyLong_AsSize_t", ret = SIZE_T, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyLong_AsSsize_t", ret = Py_ssize_t, args = {PyObject}, call = CImpl) + @CApiBuiltin(name = "PyLong_AsVoidPtr", ret = Pointer, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyLong_AsUnsignedLong", ret = UNSIGNED_LONG, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyLong_AsUnsignedLongLong", ret = UNSIGNED_LONG_LONG, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyLong_AsUnsignedLongLongMask", ret = UNSIGNED_LONG_LONG, args = {PyObject}, call = CImpl) From 33f3f4b9eb6c22aa438e25e5a089ba31fc0c3b89 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 6 May 2026 04:47:39 +0200 Subject: [PATCH 15/17] Move PyUnstable_Long_IsCompact to C implementation Move PyUnstable_Long_IsCompact from the Java-direct C API builtin to a CImpl entry in CApiFunction. Enable the public C symbol in the active GraalPy section of longobject.c. The active implementation reads GraalPyPrivate_Long_lv_tag(op) directly and applies the compact-tag predicate, because in GraalPy headers _PyLong_IsCompact macro-routes back to the public symbol. Existing cpyext coverage for compact 0, -1, 1, and an obviously non-compact large int continues to pass. Verification: mx python-jvm passed (mxbuild/buildlog-20260506-041616.html). Focused JVM tests passed: mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyUnstable_Long_IsCompact graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_long.py::tests.cpyext.test_long.TestPyLong.test_PyUnstable_Long_CompactValue, 2 tests OK. JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest LATEST_JAVA_HOME=/home/tim/.mx/jdks/labsjdk-ce-latest mx python-svm passed (mxbuild/buildlog-20260506-042143.html; native focused-test rebuild mxbuild/buildlog-20260506-042507.html). Focused native tests passed with GRAALPYTEST_ALLOW_NO_JAVA_ASSERTIONS=true, 2 tests OK. mx graalpytest graalpython/com.oracle.graal.python.test/src/tests/ passed: 2811 tests in 694.82s, OK (passed=2792, skipped=19). git diff --check passed. Temporary benchmark c-pylong-iscompact.py was used and removed before commit. Benchmarks used --experimental-options --engine.Compilation=false graalpython/com.oracle.graal.python.benchmarks/python/harness.py -r 3 -i 9 graalpython/com.oracle.graal.python.benchmarks/python/micro/c-pylong-iscompact.py . Modes: 0 compact positive tagged int; 1 non-compact large int; 2 compact negative tagged int; 3 mixed compact positive + non-compact large + compact negative. Before benchmarking, vmstat 5 13 showed 99-100% idle after the initial historical sample and ps showed no active build/test CPU-heavy process. Benchmark results: mode 0, 100000000 iterations: baseline median 4.699s (best 4.586s, worst 4.728s), current median 0.175s (best 0.171s, worst 0.176s), about 26.80x faster. Mode 1, 10000000 iterations: baseline median 0.533s (best 0.517s, worst 0.540s), current median 0.517s (best 0.507s, worst 0.520s), about 1.03x faster. Mode 2, 100000000 iterations: baseline median 4.593s (best 4.553s, worst 4.690s), current median 0.175s (best 0.171s, worst 0.184s), about 26.25x faster. Mode 3, 10000000 iterations: baseline median 1.425s (best 1.409s, worst 1.440s), current median 0.570s (best 0.553s, worst 0.584s), about 2.50x faster. Conclusion: moving PyUnstable_Long_IsCompact to CImpl improves compact tagged-int checks dramatically, keeps non-compact large-int checks slightly faster, and improves the mixed native no-compilation benchmark by about 2.50x. --- .../com.oracle.graal.python.cext/src/longobject.c | 7 +++++++ .../modules/cext/PythonCextLongBuiltins.java | 15 --------------- .../builtins/objects/cext/capi/CApiFunction.java | 1 + 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/longobject.c b/graalpython/com.oracle.graal.python.cext/src/longobject.c index 6aa2bb28ad..b2d0dc0a86 100644 --- a/graalpython/com.oracle.graal.python.cext/src/longobject.c +++ b/graalpython/com.oracle.graal.python.cext/src/longobject.c @@ -6217,6 +6217,13 @@ PyUnstable_Long_CompactValue(const PyLongObject* op) { #endif // GraalPy change +#undef PyUnstable_Long_IsCompact + +int +PyUnstable_Long_IsCompact(const PyLongObject* op) { + return GraalPyPrivate_Long_lv_tag(op) < (2 << NON_SIZE_BITS); +} + #undef PyUnstable_Long_CompactValue Py_ssize_t PyUnstable_Long_CompactValue(const PyLongObject *op) { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java index 5a24fc4fba..1ea183c41b 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java @@ -45,7 +45,6 @@ import static com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiCallPath.Ignored; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.CONST_UNSIGNED_CHAR_PTR; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.ConstCharPtrAsTruffleString; -import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.ConstPyLongObject; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Int; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.LONG_LONG; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Pointer; @@ -135,20 +134,6 @@ Object fromString(Object s, int base, } } - @CApiBuiltin(ret = Int, args = {ConstPyLongObject}, call = Direct) - abstract static class PyUnstable_Long_IsCompact extends CApiUnaryBuiltinNode { - @Specialization - @TruffleBoundary - static int doI(Object value) { - if (value instanceof Integer || value instanceof Long) { - return 1; - } else if (value instanceof PInt pInt) { - return pInt.fitsIn(PInt.MIN_LONG, PInt.MAX_LONG) ? 1 : 0; - } - return 0; - } - } - @CApiBuiltin(ret = LONG_LONG, args = {PyObject, Int, SIZE_T}, call = Ignored) abstract static class GraalPyPrivate_Long_AsPrimitive extends CApiTernaryBuiltinNode { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java index b4c771bdf5..b1442d8123 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiFunction.java @@ -337,6 +337,7 @@ public final class CApiFunction { @CApiBuiltin(name = "PyLong_AsUnsignedLongLong", ret = UNSIGNED_LONG_LONG, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyLong_AsUnsignedLongLongMask", ret = UNSIGNED_LONG_LONG, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyLong_AsUnsignedLongMask", ret = UNSIGNED_LONG, args = {PyObject}, call = CImpl) + @CApiBuiltin(name = "PyUnstable_Long_IsCompact", ret = Int, args = {ConstPyLongObject}, call = CImpl) @CApiBuiltin(name = "_PyLong_NumBits", ret = SIZE_T, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "_PyLong_Sign", ret = Int, args = {PyObject}, call = CImpl) @CApiBuiltin(name = "PyLong_FromString", ret = PyObject, args = {ConstCharPtrAsTruffleString, CHAR_PTR_LIST, Int}, call = CImpl) From 8b80e215a1b49c720b83fb8ef79faa36a185b71b Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 6 May 2026 09:20:03 +0200 Subject: [PATCH 16/17] Document failed attempts to move some C API functions to pure C --- .../builtins/modules/cext/PythonCextBytesBuiltins.java | 3 +++ .../builtins/modules/cext/PythonCextListBuiltins.java | 4 ++++ .../builtins/modules/cext/PythonCextObjectBuiltins.java | 8 ++++++++ .../builtins/modules/cext/PythonCextSetBuiltins.java | 4 ++++ .../builtins/modules/cext/PythonCextTupleBuiltins.java | 3 +++ 5 files changed, 22 insertions(+) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextBytesBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextBytesBuiltins.java index 1aeb3fce1d..a2b5466dae 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextBytesBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextBytesBuiltins.java @@ -104,6 +104,9 @@ public final class PythonCextBytesBuiltins { + /* + * Moving this to native regresses median time by about 1.84x, so leaving it here. + */ @CApiBuiltin(ret = Py_ssize_t, args = {PyObjectRawPointer}, call = Direct) static long PyBytes_Size(long objPtr) { Object obj = NativeToPythonNode.executeRawUncached(objPtr); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextListBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextListBuiltins.java index 039c33c3df..ff5f88b6a5 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextListBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextListBuiltins.java @@ -239,6 +239,10 @@ Object fallback(Object list, @SuppressWarnings("unused") Object iterable) { } } + /* + * A pure-C Py_SIZE implementation regressed mixed managed/native/list-subclass workload by + * about 1.26x. + */ @CApiBuiltin(ret = Py_ssize_t, args = {PyObject}, call = Direct) abstract static class PyList_Size extends CApiUnaryBuiltinNode { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java index 0565c8474a..e478b59271 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java @@ -477,6 +477,10 @@ static int hasAttr(Object obj, Object attr, } } + /* + * Moving this to pure-C regresses, because creating the TypeError through the C error API is + * slower than upcalling and raising from Java. + */ @CApiBuiltin(ret = Py_hash_t, args = {PyObject}, call = Direct) abstract static class PyObject_HashNotImplemented extends CApiUnaryBuiltinNode { @Specialization @@ -683,6 +687,10 @@ static long GraalPyPrivate_Object_GetIter(long objectPtr) { return PythonToNativeNewRefNode.executeLongUncached(result); } + /* + * Moving this to pure C is much faster (100x) for true native tp_hash, but hashing managed + * objects (e.g. Strings) regresses (also by ~100x). + */ @CApiBuiltin(ret = Py_hash_t, args = {PyObject}, call = Direct) abstract static class PyObject_Hash extends CApiUnaryBuiltinNode { @Specialization diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextSetBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextSetBuiltins.java index f72dbdd14e..6331637ce2 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextSetBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextSetBuiltins.java @@ -287,6 +287,10 @@ static int add(Object self, @SuppressWarnings("unused") Object o, } } + /* + * A pure-C implementation regressed a mixed managed/native set/frozenset workload by about + * 1.16x. + */ @CApiBuiltin(ret = Py_ssize_t, args = {PyObject}, call = Direct) abstract static class PySet_Size extends CApiUnaryBuiltinNode { @Specialization diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextTupleBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextTupleBuiltins.java index fc3bbebe05..05fdb0097c 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextTupleBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextTupleBuiltins.java @@ -172,6 +172,9 @@ private static int checkIndex(Node inliningTarget, long key, SequenceStorage seq } } + /* + * The best attempt at moving this to pure C regressed by about 1.09x by median time. + */ @CApiBuiltin(ret = Py_ssize_t, args = {PyObject}, call = Direct) abstract static class PyTuple_Size extends CApiUnaryBuiltinNode { @Specialization From d8f474c081a6dfc7e55096392c3bd2c12b64e72a Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 6 May 2026 15:53:28 +0200 Subject: [PATCH 17/17] addressing review comments --- .../com.oracle.graal.python.cext/src/abstract.c | 8 +++++--- graalpython/com.oracle.graal.python.cext/src/object.c | 1 + .../builtins/modules/cext/PythonCextLongBuiltins.java | 10 +--------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/abstract.c b/graalpython/com.oracle.graal.python.cext/src/abstract.c index b2facc99af..c7aaa5b41b 100644 --- a/graalpython/com.oracle.graal.python.cext/src/abstract.c +++ b/graalpython/com.oracle.graal.python.cext/src/abstract.c @@ -1465,14 +1465,15 @@ _PyNumber_Index(PyObject *item) return null_error(); } + if (PyLong_Check(item)) { + return Py_NewRef(item); + } + // GraalPy change: upcall for managed objects if (points_to_py_handle_space(item)) { return GraalPyPrivate_PyNumber_Index(item); } - if (PyLong_Check(item)) { - return Py_NewRef(item); - } if (!_PyIndex_Check(item)) { PyErr_Format(PyExc_TypeError, "'%.200s' object cannot be interpreted " @@ -2890,6 +2891,7 @@ _PyObject_RealIsSubclass(PyObject *derived, PyObject *cls) PyObject * PyObject_GetIter(PyObject *o) { + // GraalPy change: upcall for managed objects if (points_to_py_handle_space(o)) { return GraalPyPrivate_Object_GetIter(o); } diff --git a/graalpython/com.oracle.graal.python.cext/src/object.c b/graalpython/com.oracle.graal.python.cext/src/object.c index 54df3cc003..449cd46818 100644 --- a/graalpython/com.oracle.graal.python.cext/src/object.c +++ b/graalpython/com.oracle.graal.python.cext/src/object.c @@ -1789,6 +1789,7 @@ PyObject_IsTrue(PyObject *v) return 0; if (v == Py_None) return 0; + // GraalPy change: upcall for managed objects if (points_to_py_handle_space(v)) return GraalPyPrivate_Object_IsTrue(v); else if (Py_TYPE(v)->tp_as_number != NULL && diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java index 1ea183c41b..d4ee5628ba 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextLongBuiltins.java @@ -59,8 +59,6 @@ import static com.oracle.graal.python.runtime.nativeaccess.NativeMemory.readByteArrayElements; import static com.oracle.graal.python.runtime.exception.PythonErrorType.OverflowError; -import java.math.BigInteger; - import com.oracle.graal.python.PythonLanguage; import com.oracle.graal.python.builtins.PythonBuiltinClassType; import com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApi5BuiltinNode; @@ -92,7 +90,6 @@ import com.oracle.graal.python.util.OverflowException; import com.oracle.graal.python.util.PythonUtils; import com.oracle.truffle.api.CompilerDirectives; -import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Bind; import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Cached.Exclusive; @@ -185,15 +182,10 @@ static long doSignedLong(long n) { @CApiBuiltin(ret = PyObjectRawPointer, args = {UNSIGNED_LONG_LONG}, call = Ignored, acquireGil = false) static long GraalPyPrivate_Long_FromUnsignedLongLong(long n) { - Object result = n >= 0 ? n : PFactory.createInt(PythonLanguage.get(null), convertToBigInteger(n)); + Object result = n >= 0 ? n : PFactory.createInt(PythonLanguage.get(null), PInt.longToUnsignedBigInteger(n)); return PythonToNativeNewRefNode.executeLongUncached(result); } - @TruffleBoundary - private static BigInteger convertToBigInteger(long n) { - return BigInteger.valueOf(n).add(BigInteger.ONE.shiftLeft(Long.SIZE)); - } - @CApiBuiltin(ret = SIZE_T, args = {PyObjectRawPointer}, call = Ignored, acquireGil = false) static long GraalPyPrivate_Long_NumBits(long objPtr) { Object obj = NativeToPythonNode.executeRawUncached(objPtr);