diff --git a/.gitignore b/.gitignore index 441bbb6b..69526892 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ firefox-102.2.0/* firefox-*/* firefox-*/ tests/__pycache__/* -tests/python/__pycache__/* \ No newline at end of file +tests/python/__pycache__/* +Testing/Temporary \ No newline at end of file diff --git a/include/IntType.hh b/include/IntType.hh index ded6bf7e..36cf5c60 100644 --- a/include/IntType.hh +++ b/include/IntType.hh @@ -1,11 +1,11 @@ /** * @file IntType.hh - * @author Caleb Aikens (caleb@distributive.network) & Giovanni Tedesco (giovanni@distributive.network) + * @author Caleb Aikens (caleb@distributive.network) & Giovanni Tedesco (giovanni@distributive.network) & Tom Tang (xmader@distributive.network) * @brief Struct for representing python ints - * @version 0.1 - * @date 2022-07-27 + * @version 0.2 + * @date 2023-03-16 * - * @copyright Copyright (c) 2022 + * @copyright Copyright (c) 2023 * */ @@ -15,19 +15,36 @@ #include "PyType.hh" #include "TypeEnum.hh" +#include + #include #include /** - * @brief This struct represents the 'int' type in Python, which is represented as a 'long' in C++. It inherits from the PyType struct + * @brief This struct represents the 'int' type (arbitrary-precision) in Python. It inherits from the PyType struct */ struct IntType : public PyType { public: IntType(PyObject *object); IntType(long n); + + /** + * @brief Construct a new IntType object from a JS::BigInt. + * + * @param cx - javascript context pointer + * @param str - JS::BigInt pointer + */ + IntType(JSContext *cx, JS::BigInt *bigint); + const TYPE returnType = TYPE::INT; - long getValue() const; + + /** + * @brief Convert the IntType object to a JS::BigInt + * + * @param cx - javascript context pointer + */ + JS::BigInt *toJsBigInt(JSContext *cx); protected: virtual void print(std::ostream &os) const override; diff --git a/include/modules/explore/explore.hh b/include/modules/explore/explore.hh deleted file mode 100644 index 66aecd60..00000000 --- a/include/modules/explore/explore.hh +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef PythonMonkey_Explore_ -#define PythonMonkey_Explore_ - -#include "include/IntType.hh" -#include "include/ListType.hh" - -#include - -/** @brief Function that takes an arbitrary number of arguments from python and outputs their C/C++ values. - - @author Giovanni Tedesco & Caleb Aikens - @date July 2022 - - @param self - Pointer to the python environment - @param args - The PyTuple of arguments that are passed into the function - */ -static PyObject *output(PyObject *self, PyObject *args); -static PyObject *factor(PyObject *self, PyObject *args); - -/** - * @brief - * - * @param self - Pointer to the python environment - * @param args - The PyTuple of arguments that are passed into the function - * @return PyObject* - */ -static PyObject *run(PyObject *self, PyObject *args); - -/** - * @brief Function that factors an integer in python - * - * @param self - Pointer to python environment - * @param args - The PyTuple of arugments that are passed into the function - * @return PyObject* - */ -static PyObject *pfactor(PyObject *self, PyObject *args); - -ListType *factor_int(IntType *x); - -#endif \ No newline at end of file diff --git a/include/modules/pythonmonkey/pythonmonkey.hh b/include/modules/pythonmonkey/pythonmonkey.hh index 9f1a5d32..4e051d0a 100644 --- a/include/modules/pythonmonkey/pythonmonkey.hh +++ b/include/modules/pythonmonkey/pythonmonkey.hh @@ -19,7 +19,8 @@ #include -#define PythonMonkey_Null PyObject_GetAttrString(PyState_FindModule(&pythonmonkey), "null") /**< macro for python null object*/ +#define PythonMonkey_Null PyObject_GetAttrString(PyState_FindModule(&pythonmonkey), "null") /**< macro for pythonmonkey.null object*/ +#define PythonMonkey_BigInt PyObject_GetAttrString(PyState_FindModule(&pythonmonkey), "bigint") /**< macro for pythonmonkey.bigint class object */ static JSContext *cx; /**< pointer to PythonMonkey's JSContext */ static JS::Rooted *global; /**< pointer to the global object of PythonMonkey's JSContext */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2e3d8357..fb9e7eca 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,12 +1,7 @@ include_directories(${CMAKE_CURRENT_LIST_DIR}) -#list(APPEND EXPLORE_SOURCE_FILES ${SOURCE_FILES} "${CMAKE_SOURCE_DIR}/src/modules/explore/explore.cc") list(APPEND PYTHONMONKEY_SOURCE_FILES ${SOURCE_FILES} "${CMAKE_SOURCE_DIR}/src/modules/pythonmonkey/pythonmonkey.cc") -# add_library(explore SHARED -# ${EXPLORE_SOURCE_FILES} -# ) - add_library(pythonmonkey SHARED ${PYTHONMONKEY_SOURCE_FILES} ) @@ -16,18 +11,9 @@ execute_process( OUTPUT_VARIABLE pyloc ) -# target_include_directories(explore PUBLIC ..) target_include_directories(pythonmonkey PUBLIC ..) if(WIN32) - # set_target_properties( - # explore - # PROPERTIES - # PREFIX "" - # SUFFIX ".pyd" - # OUTPUT_NAME "explore" - # CXX_STANDARD 17 - # ) set_target_properties( pythonmonkey PROPERTIES @@ -37,12 +23,6 @@ if(WIN32) CXX_STANDARD 17 ) elseif(UNIX) - # set_target_properties( - # explore - # PROPERTIES - # PREFIX "" - # SUFFIX ".so" - # ) set_target_properties( pythonmonkey PROPERTIES @@ -50,10 +30,9 @@ elseif(UNIX) SUFFIX ".so" ) endif() -# target_link_libraries(explore ${PYTHON_LIBRARIES}) + target_link_libraries(pythonmonkey ${PYTHON_LIBRARIES}) target_link_libraries(pythonmonkey ${SPIDERMONKEY_LIBRARIES}) -# target_include_directories(explore PRIVATE ${PYTHON_INCLUDE_DIR}) target_include_directories(pythonmonkey PRIVATE ${PYTHON_INCLUDE_DIR}) target_include_directories(pythonmonkey PRIVATE ${SPIDERMONKEY_INCLUDE_DIR}) \ No newline at end of file diff --git a/src/IntType.cc b/src/IntType.cc index bbe42503..cb8cdb18 100644 --- a/src/IntType.cc +++ b/src/IntType.cc @@ -1,21 +1,147 @@ +#include "include/modules/pythonmonkey/pythonmonkey.hh" + #include "include/IntType.hh" #include "include/PyType.hh" #include "include/TypeEnum.hh" +#include +#include + #include #include +#include + +#define SIGN_BIT_MASK 0b1000 // https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/src/vm/BigIntType.h#l40 +#define CELL_HEADER_LENGTH 8 // https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/src/gc/Cell.h#l602 + +#define JS_DIGIT_BIT JS_BITS_PER_WORD +#define PY_DIGIT_BIT PYLONG_BITS_IN_DIGIT + +#define js_digit_t uintptr_t // https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/src/vm/BigIntType.h#l36 +#define JS_DIGIT_BYTE (sizeof(js_digit_t)/sizeof(uint8_t)) +#define JS_INLINE_DIGIT_MAX_LEN 1 // https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/src/vm/BigIntType.h#l43 + +static const char HEX_CHAR_LOOKUP_TABLE[] = "0123456789ABCDEF"; IntType::IntType(PyObject *object) : PyType(object) {} IntType::IntType(long n) : PyType(Py_BuildValue("i", n)) {} -long IntType::getValue() const { - return PyLong_AS_LONG(pyObject); +IntType::IntType(JSContext *cx, JS::BigInt *bigint) { + // Get the sign bit + bool isNegative = BigIntIsNegative(bigint); + + // Read the digits count in this JS BigInt + // see https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/src/vm/BigIntType.h#l48 + // https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/src/gc/Cell.h#l623 + uint32_t jsDigitCount = ((uint32_t *)bigint)[1]; + + // Get all the 64-bit (assuming we compile on 64-bit OS) "digits" from JS BigInt + js_digit_t *jsDigits = (js_digit_t *)(((char *)bigint) + CELL_HEADER_LENGTH); + if (jsDigitCount > JS_INLINE_DIGIT_MAX_LEN) { // hasHeapDigits + // We actually have a pointer to the digit storage if the number cannot fit in one uint64_t + // see https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/src/vm/BigIntType.h#l54 + jsDigits = *((js_digit_t **)jsDigits); + } + // + // The digit storage starts with the least significant digit (little-endian digit order). + // Byte order within a digit is native-endian. + + if constexpr (std::endian::native == std::endian::big) { // C++20 + // @TODO (Tom Tang): use C++23 std::byteswap? + printf("big-endian cpu is not supported by PythonMonkey yet"); + return; + } + // If the native endianness is also little-endian, + // we now have consecutive bytes of 8-bit "digits" in little-endian order + const uint8_t *bytes = const_cast((uint8_t *)jsDigits); + if (jsDigitCount == 0) { + // Create a new object instead of reusing the object for int 0 + // see https://github.com/python/cpython/blob/3.9/Objects/longobject.c#L862 + // https://github.com/python/cpython/blob/3.9/Objects/longobject.c#L310 + pyObject = (PyObject *)_PyLong_New(0); + } else { + pyObject = _PyLong_FromByteArray(bytes, jsDigitCount * JS_DIGIT_BYTE, true, false); + } + + // Set the sign bit + // see https://github.com/python/cpython/blob/3.9/Objects/longobject.c#L956 + if (isNegative) { + ssize_t pyDigitCount = Py_SIZE(pyObject); + Py_SET_SIZE(pyObject, -pyDigitCount); + } + + // Cast to a pythonmonkey.bigint to differentiate it from a normal Python int, + // allowing Py<->JS two-way BigInt conversion + Py_SET_TYPE(pyObject, (PyTypeObject *)(PythonMonkey_BigInt)); +} + +JS::BigInt *IntType::toJsBigInt(JSContext *cx) { + // Figure out how many 64-bit "digits" we would have for JS BigInt + // see https://github.com/python/cpython/blob/3.9/Modules/_randommodule.c#L306 + size_t bitCount = _PyLong_NumBits(pyObject); + if (bitCount == (size_t)-1 && PyErr_Occurred()) + return nullptr; + uint32_t jsDigitCount = bitCount == 0 ? 1 : (bitCount - 1) / JS_DIGIT_BIT + 1; + // Get the sign bit + // see https://github.com/python/cpython/blob/3.9/Objects/longobject.c#L977 + ssize_t pyDigitCount = Py_SIZE(pyObject); // negative on negative numbers + bool isNegative = pyDigitCount < 0; + // Force to make the number positive otherwise _PyLong_AsByteArray would complain + // see https://github.com/python/cpython/blob/3.9/Objects/longobject.c#L980 + if (isNegative) { + Py_SET_SIZE(pyObject, /*abs()*/ -pyDigitCount); + } + + JS::BigInt *bigint = nullptr; + if (jsDigitCount <= 1) { + // Fast path for int fits in one js_digit_t (uint64 on 64-bit OS) + bigint = JS::detail::BigIntFromUint64(cx, PyLong_AsUnsignedLongLong(pyObject)); + } else { + // Convert to bytes of 8-bit "digits" in **big-endian** order + size_t byteCount = (size_t)JS_DIGIT_BYTE * jsDigitCount; + uint8_t *bytes = (uint8_t *)PyMem_Malloc(byteCount); + _PyLong_AsByteArray((PyLongObject *)pyObject, bytes, byteCount, /*is_little_endian*/ false, false); + + // Convert pm.bigint to JS::BigInt through hex strings (no public API to convert directly through bytes) + // TODO (Tom Tang): We could manually allocate the memory, https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/src/vm/BigIntType.cpp#l162, but still no public API + // TODO (Tom Tang): Could we fill in an object with similar memory alignment (maybe by NewArrayBufferWithContents), and coerce it to BigInt? + + // Calculate the number of chars required to represent the bigint in hex string + size_t charCount = byteCount * 2; + // Convert bytes to hex string (big-endian) + std::vector chars = std::vector(charCount); // can't be null-terminated, otherwise SimpleStringToBigInt would read the extra \0 character and then segfault + for (size_t i = 0, j = 0; i < charCount; i += 2, j++) { + chars[i] = HEX_CHAR_LOOKUP_TABLE[(bytes[j] >> 4)&0xf]; // high nibble + chars[i+1] = HEX_CHAR_LOOKUP_TABLE[bytes[j]&0xf]; // low nibble + } + PyMem_Free(bytes); + + // Convert hex string to JS::BigInt + mozilla::Span strSpan = mozilla::Span(chars); // storing only a pointer to the underlying array and length + bigint = JS::SimpleStringToBigInt(cx, strSpan, 16); + } + + if (isNegative) { + // Make negative number back negative + // TODO (Tom Tang): use _PyLong_Copy to create a new object. Not thread-safe here + Py_SET_SIZE(pyObject, pyDigitCount); + + // Set the sign bit + // https://hg.mozilla.org/releases/mozilla-esr102/file/tip/js/src/vm/BigIntType.cpp#l1801 + /* flagsField */ ((uint32_t *)bigint)[0] |= SIGN_BIT_MASK; + } + + return bigint; } void IntType::print(std::ostream &os) const { - os << this->getValue(); + // Making sure the value does not overflow even if the int has millions of bits of precision + PyObject *str = PyObject_Str(pyObject); + os << PyUnicode_AsUTF8(str); + // https://pythonextensionpatterns.readthedocs.io/en/latest/refcount.html#new-references + Py_DECREF(str); // free } \ No newline at end of file diff --git a/src/jsTypeFactory.cc b/src/jsTypeFactory.cc index f2d2d7b3..e0d1012d 100644 --- a/src/jsTypeFactory.cc +++ b/src/jsTypeFactory.cc @@ -16,6 +16,7 @@ #include "include/FuncType.hh" #include "include/pyTypeFactory.hh" #include "include/StrType.hh" +#include "include/IntType.hh" #include #include @@ -65,7 +66,15 @@ JS::Value jsTypeFactory(JSContext *cx, PyObject *object) { returnType.setBoolean(PyLong_AsLong(object)); } else if (PyLong_Check(object)) { - returnType.setNumber(PyLong_AsLong(object)); + if (PyObject_IsInstance(object, PythonMonkey_BigInt)) { // pm.bigint is a subclass of the builtin int type + JS::BigInt *bigint = IntType(object).toJsBigInt(cx); + returnType.setBigInt(bigint); + } else if (_PyLong_NumBits(object) <= 53) { // num <= JS Number.MAX_SAFE_INTEGER, the mantissa of a float64 is 53 bits (with 52 explicitly stored and the highest bit always being 1) + uint64_t num = PyLong_AsLongLong(object); + returnType.setNumber(num); + } else { + PyErr_SetString(PyExc_OverflowError, "Absolute value of the integer exceeds JS Number.MAX_SAFE_INTEGER. Use pythonmonkey.bigint instead."); + } } else if (PyFloat_Check(object)) { returnType.setNumber(PyFloat_AsDouble(object)); @@ -127,7 +136,7 @@ JS::Value jsTypeFactory(JSContext *cx, PyObject *object) { returnType.setNull(); } else { - PyErr_SetString(PyExc_TypeError, "Python types other than bool, int, float, str, None, and our custom Null type are not supported by pythonmonkey yet."); + PyErr_SetString(PyExc_TypeError, "Python types other than bool, int, pythonmonkey.bigint, float, str, None, and our custom Null type are not supported by pythonmonkey yet."); } return returnType; diff --git a/src/modules/explore/explore.cc b/src/modules/explore/explore.cc deleted file mode 100644 index b36a74e5..00000000 --- a/src/modules/explore/explore.cc +++ /dev/null @@ -1,103 +0,0 @@ -#include "include/modules/explore/explore.hh" - -#include "include/FuncType.hh" -#include "include/IntType.hh" -#include "include/ListType.hh" -#include "include/PyEvaluator.hh" -#include "include/pyTypeFactory.hh" -#include "include/StrType.hh" -#include "include/TupleType.hh" -#include "include/utilities.hh" - -#include - -#include - -#include - -/** - * @brief Factors an IntType - * - * @param x The IntType representation of the integer you want to factor - * @return PyObject* a list which is not referenced by the python garbage collector - */ -ListType *factor_int(IntType *x) { - ListType *list = new ListType(); - int n = x->getValue(); - - for (int i = 1; i < sqrt(n); i++) { - if (n % i == 0) { - IntType *a = new IntType(i); - IntType *b = new IntType(n/i); - - list->append(a); - list->append(b); - } - } - - list->sort(); - - return list; -} - -static PyObject *output(PyObject *self, PyObject *args) { - const int size = PyTuple_Size(args); - for (int i = 0; i < size; i++) { - PyType *item = pyTypeFactory(PyTuple_GET_ITEM(args, i)); - - std::cout << *item << std::endl; - } - Py_RETURN_NONE; -} - -static PyObject *factor(PyObject *self, PyObject *args) { - IntType *input = new IntType(PyTuple_GetItem(args, 0)); - - return factor_int(input)->getPyObject(); -} - -static PyObject *pfactor(PyObject *self, PyObject *args) { - PyEvaluator p = PyEvaluator(); - TupleType *arguments = new TupleType(args); - - PyType *result = p.eval("import math\ndef f(n):\n\treturn [x for x in range(1, n + 1) if n % x == 0]\n", "pfactor", arguments); - - if (result) { - return result->getPyObject(); - } - else { - return NULL; - } -} - -static PyObject *run(PyObject *self, PyObject *args) { - PyEvaluator p = PyEvaluator(); - - StrType *input = new StrType(PyTuple_GetItem(args, 0)); - - p.eval(input->getValue()); - - Py_RETURN_NONE; -} - -static PyMethodDef ExploreMethods[] = { - {"output", output, METH_VARARGS, "Multivariatic function outputs"}, - {"factor", factor, METH_VARARGS, "Factor a python integer in C++"}, - {"pfactor", pfactor, METH_VARARGS, "Factor a python integer in C++ using python"}, - {"run", run, METH_VARARGS, "Run an arbirtrary python command in c++"}, - {NULL, NULL, 0, NULL} -}; - -static struct PyModuleDef explore = -{ - PyModuleDef_HEAD_INIT, - "explore", /* name of module */ - "", /* module documentation, may be NULL */ - -1, /* size of per-interpreter state of the module, or -1 if the module keeps state in global variables. */ - ExploreMethods -}; - -PyMODINIT_FUNC PyInit_explore(void) -{ - return PyModule_Create(&explore); -} \ No newline at end of file diff --git a/src/modules/pythonmonkey/pythonmonkey.cc b/src/modules/pythonmonkey/pythonmonkey.cc index 37286140..c5ceb424 100644 --- a/src/modules/pythonmonkey/pythonmonkey.cc +++ b/src/modules/pythonmonkey/pythonmonkey.cc @@ -30,57 +30,21 @@ typedef struct { std::unordered_map *>> PyTypeToGCThing; /**< data structure to hold memoized PyObject & GCThing data for handling GC*/ -// @TODO (Caleb Aikens) figure out how to use C99-style designated initializers with a modern C++ compiler static PyTypeObject NullType = { .ob_base = PyVarObject_HEAD_INIT(NULL, 0) - .tp_name = "pythonmonkey.Null", + .tp_name = "pythonmonkey.null", .tp_basicsize = sizeof(NullObject), - .tp_itemsize = 0, - .tp_dealloc = NULL, - .tp_vectorcall_offset = NULL, - .tp_getattr = NULL, - .tp_setattr = NULL, - .tp_as_async = NULL, - .tp_repr = NULL, - .tp_as_number = NULL, - .tp_as_sequence = NULL, - .tp_as_mapping = NULL, - .tp_hash = NULL, - .tp_call = NULL, - .tp_str = NULL, - .tp_getattro = NULL, - .tp_setattro = NULL, - .tp_as_buffer = NULL, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = PyDoc_STR("Javascript null object"), - .tp_traverse = NULL, - .tp_clear = NULL, - .tp_richcompare = NULL, - .tp_weaklistoffset = NULL, - .tp_iter = NULL, - .tp_iternext = NULL, - .tp_methods = NULL, - .tp_members = NULL, - .tp_getset = NULL, - .tp_base = NULL, - .tp_dict = NULL, - .tp_descr_get = NULL, - .tp_descr_set = NULL, - .tp_dictoffset = NULL, - .tp_init = NULL, - .tp_alloc = NULL, - .tp_new = PyType_GenericNew, - .tp_free = NULL, - .tp_is_gc = NULL, - .tp_bases = NULL, - .tp_mro = NULL, - .tp_cache = NULL, - .tp_subclasses = NULL, - .tp_weaklist = NULL, - .tp_del = NULL, - .tp_version_tag = NULL, - .tp_finalize = NULL, - .tp_vectorcall = NULL, +}; + +static PyTypeObject BigIntType = { + .tp_name = "pythonmonkey.bigint", + .tp_flags = Py_TPFLAGS_DEFAULT + | Py_TPFLAGS_LONG_SUBCLASS // https://docs.python.org/3/c-api/typeobj.html#Py_TPFLAGS_LONG_SUBCLASS + | Py_TPFLAGS_BASETYPE, // can be subclassed + .tp_doc = PyDoc_STR("Javascript BigInt object"), + .tp_base = &PyLong_Type, // extending the builtin int type }; static void cleanup() { @@ -238,6 +202,8 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void) PyObject *pyModule; if (PyType_Ready(&NullType) < 0) return NULL; + if (PyType_Ready(&BigIntType) < 0) + return NULL; pyModule = PyModule_Create(&pythonmonkey); if (pyModule == NULL) @@ -249,6 +215,12 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void) Py_DECREF(pyModule); return NULL; } + Py_INCREF(&BigIntType); + if (PyModule_AddObject(pyModule, "bigint", (PyObject *)&BigIntType) < 0) { + Py_DECREF(&BigIntType); + Py_DECREF(pyModule); + return NULL; + } SpiderMonkeyError = PyErr_NewException("pythonmonkey.SpiderMonkeyError", NULL, NULL); if (PyModule_AddObject(pyModule, "SpiderMonkeyError", SpiderMonkeyError)) { diff --git a/src/pyTypeFactory.cc b/src/pyTypeFactory.cc index a855973c..d80e653a 100644 --- a/src/pyTypeFactory.cc +++ b/src/pyTypeFactory.cc @@ -75,7 +75,7 @@ PyType *pyTypeFactory(JSContext *cx, JS::Rooted *global, JS::Rooted< printf("symbol type is not handled by PythonMonkey yet"); } else if (rval->isBigInt()) { - printf("bigint type is not handled by PythonMonkey yet"); + returnValue = new IntType(cx, rval->toBigInt()); } else if (rval->isObject()) { JS::Rooted obj(cx); @@ -84,6 +84,8 @@ PyType *pyTypeFactory(JSContext *cx, JS::Rooted *global, JS::Rooted< JS::GetBuiltinClass(cx, obj, &cls); switch (cls) { case js::ESClass::Boolean: { + // TODO (Caleb Aikens): refactor out all `js::Unbox` calls + // TODO (Caleb Aikens): refactor using recursive call to `pyTypeFactory` JS::RootedValue unboxed(cx); js::Unbox(cx, obj, &unboxed); returnValue = new BoolType(unboxed.toBoolean()); @@ -108,6 +110,12 @@ PyType *pyTypeFactory(JSContext *cx, JS::Rooted *global, JS::Rooted< returnValue = new FloatType(unboxed.toNumber()); break; } + case js::ESClass::BigInt: { + JS::RootedValue unboxed(cx); + js::Unbox(cx, obj, &unboxed); + returnValue = new IntType(cx, unboxed.toBigInt()); + break; + } case js::ESClass::String: { JS::RootedValue unboxed(cx); js::Unbox(cx, obj, &unboxed); @@ -136,6 +144,9 @@ static PyObject *callJSFunc(PyObject *JSCxGlobalFuncTuple, PyObject *args) { JS::RootedVector JSargsVector(JScontext); for (size_t i = 0; i < PyTuple_Size(args); i++) { JS::Value jsValue = jsTypeFactory(JScontext, PyTuple_GetItem(args, i)); + if (PyErr_Occurred()) { // Check if an exception has already been set in the flow of control + return NULL; // Fail-fast + } JSargsVector.append(jsValue); } diff --git a/tests/python/test_explore.py b/tests/python/test_explore.py deleted file mode 100644 index a950edae..00000000 --- a/tests/python/test_explore.py +++ /dev/null @@ -1,29 +0,0 @@ -# import explore - - -# def test_passes(): -# assert True - - -# def test_output_outputs_correct_integers(capfd): -# explore.output(1, 65, 72) - -# out, err = capfd.readouterr() -# assert out == "16572" - - -# def test_output_outputs_correct_strings(capfd): - -# explore.output("abcd", "newline\n", "tabs\t") - -# out, err = capfd.readouterr() -# assert out == "abcdnewline\ntabs\t" - - -# def test_output_outputs_correct_mixture(capfd): - -# explore.output("abcd", 13, "some string\n", 65) - -# out, err = capfd.readouterr() - -# assert out == "abcd13some string\n65" diff --git a/tests/python/test_pythonmonkey_eval.py b/tests/python/test_pythonmonkey_eval.py index 48b1bf8c..3a774f50 100644 --- a/tests/python/test_pythonmonkey_eval.py +++ b/tests/python/test_pythonmonkey_eval.py @@ -1,3 +1,4 @@ +import pytest import pythonmonkey as pm import gc import random @@ -146,6 +147,49 @@ def test_eval_numbers_integers(): js_number = pm.eval(repr(py_number)) assert py_number == js_number +def test_eval_numbers_bigints(): + def test_bigint(py_number: int): + js_number = pm.eval(f'{repr(py_number)}n') + assert py_number == js_number + + test_bigint(0) + test_bigint(1) + test_bigint(-1) + + # CPython would reuse the objects for small ints in range [-5, 256] + # Making sure we don't do any changes on them + def test_cached_int_object(py_number): + # type is still int + assert type(py_number) == int + assert type(py_number) != pm.bigint + test_bigint(py_number) + assert type(py_number) == int + assert type(py_number) != pm.bigint + # the value doesn't change + # TODO (Tom Tang): Find a way to create a NEW int object with the same value, because int literals also reuse the cached int objects + for _ in range(2): + test_cached_int_object(0) # _PyLong_FromByteArray reuses the int 0 object, + # see https://github.com/python/cpython/blob/3.9/Objects/longobject.c#L862 + for i in range(10): + test_cached_int_object(random.randint(-5, 256)) + + test_bigint(18014398509481984) # 2**54 + test_bigint(-18014398509481984) # -2**54 + test_bigint(18446744073709551615) # 2**64-1 + test_bigint(18446744073709551616) # 2**64 + test_bigint(-18446744073709551617) # -2**64-1 + + limit = 2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397376 + # = 2**300 + for i in range(10): + py_number = random.randint(-limit, limit) + test_bigint(py_number) + + # TODO (Tom Tang): test -0 (negative zero) + # There's no -0 in both Python int and JS BigInt, + # but this could be possible in JS BigInt's internal representation as it uses a sign bit flag. + # On the other hand, Python int uses `ob_size` 0 for 0, >0 for positive values, <0 for negative values + def test_eval_booleans(): py_bool = True js_bool = pm.eval('true') @@ -186,6 +230,23 @@ def test_eval_boxed_numbers_integers(): js_number = pm.eval(f'new Number({repr(py_number)})') assert py_number == js_number +def test_eval_boxed_numbers_bigints(): + def test_boxed_bigint(py_number: int): + # `BigInt()` can only be called without `new` + # https://tc39.es/ecma262/#sec-bigint-constructor + js_number = pm.eval(f'new Object({repr(py_number)}n)') + assert py_number == js_number + + test_boxed_bigint(0) + test_boxed_bigint(1) + test_boxed_bigint(-1) + + limit = 2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397376 + # = 2**300 + for i in range(10): + py_number = random.randint(-limit, limit) + test_boxed_bigint(py_number) + def test_eval_boxed_ascii_string_matches_evaluated_string(): py_ascii_string = "abc" js_ascii_string = pm.eval(f'new String({repr(py_ascii_string)})') @@ -359,6 +420,99 @@ def test_eval_functions_latin1_string_args(): assert concatenate(string1, string2) == (string1 + string2) +def test_eval_functions_bigints(): + ident = pm.eval("(a) => { return a }") + add = pm.eval("(a, b) => { return a + b }") + + int1 = random.randint(-1000000,1000000) + bigint1 = pm.bigint(int1) + assert int1 == bigint1 + + # should return pm.bigint + assert type(ident(bigint1)) == pm.bigint + assert ident(bigint1) is not bigint1 + # should return float (because JS number is float64) + assert type(ident(int1)) == float + assert ident(int1) == ident(bigint1) + + # should raise exception on ints > (2^53-1), or < -(2^53-1) + def not_raise(num): + ident(num) + def should_raise(num): + with pytest.raises(OverflowError, match="Use pythonmonkey.bigint instead"): + ident(num) + not_raise(9007199254740991) # 2**53-1, 0x433_FFFFFFFFFFFFF in float64 + should_raise(9007199254740992) # 2**53, 0x434_0000000000000 in float64 + should_raise(9007199254740993) # 2**53+1, NOT 0x434_0000000000001 (2**53+2) + not_raise(-9007199254740991) # -(2**53-1) + should_raise(-9007199254740992) # -(2**53) + should_raise(-9007199254740993) # -(2**53+1) + + # should also raise exception on large integers (>=2**53) that can be exactly represented by a float64 + # in our current implementation + should_raise(9007199254740994) # 2**53+2, 0x434_0000000000001 in float64 + should_raise(2**61+2**9) # 0x43C_0000000000001 in float64 + + # should raise "Use pythonmonkey.bigint" instead of `PyLong_AsLongLong`'s "OverflowError: int too big to convert" on ints larger than 64bits + should_raise(2**65) + should_raise(-2**65) + not_raise(pm.bigint(2**65)) + not_raise(pm.bigint(-2**65)) + + # should raise JS error when mixing a BigInt with a number in arithmetic operations + def should_js_error(a, b): + with pytest.raises(pm.SpiderMonkeyError, match="can't convert BigInt to number"): + add(a, b) + should_js_error(pm.bigint(0), 0) + should_js_error(pm.bigint(1), 2) + should_js_error(3, pm.bigint(4)) + should_js_error(-5, pm.bigint(6)) + + assert add(pm.bigint(0), pm.bigint(0)) == 0 + assert add(pm.bigint(1), pm.bigint(0)) == 1 + assert add(pm.bigint(1), pm.bigint(2)) == 3 + assert add(pm.bigint(-1), pm.bigint(1)) == 0 + assert add(pm.bigint(2**60), pm.bigint(0)) == 1152921504606846976 + assert add(pm.bigint(2**65), pm.bigint(-2**65-1)) == -1 + + # fuzztest + limit = 2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397376 # 2**300 + for i in range(10): + num1 = random.randint(-limit, limit) + num2 = random.randint(-limit, limit) + assert add(pm.bigint(num1), pm.bigint(num2)) == num1+num2 + +def test_eval_functions_bigint_factorial(): + factorial = pm.eval("(num) => {let r = 1n; for(let i = 0n; i Number.MAX_SAFE_INTEGER + assert factorial(pm.bigint(21)) == 51090942171709440000 # > 64 bit int + assert factorial(pm.bigint(35)) == 10333147966386144929666651337523200000000 # > 128 bit + +def test_eval_functions_bigint_crc32(): + crc_table_at = pm.eval(""" + // translated from https://rosettacode.org/wiki/CRC-32#Python + const crc_table = (function create_table() { + const a = [] + for (let i = 0n; i < 256n; i++) { + let k = i + for (let j = 0n; j < 8n; j++) { + // must use bigint here as js number is trimmed to int32 in bitwise operations + if (k & 1n) k ^= 0x1db710640n + k >>= 1n + } + a.push(k) + } + return a + })(); + (n) => crc_table[n] + """) + assert type(crc_table_at(1)) == pm.bigint + assert crc_table_at(0) == 0 + assert crc_table_at(1) == 1996959894 + assert crc_table_at(255) == 755167117 # last item + def test_eval_functions_ucs2_string_args(): concatenate = pm.eval("(a, b) => { return a + b}") n = 10