diff --git a/out/jl2py/csrc/jl2py.h b/out/jl2py/csrc/jl2py.h index 6dde085..0014c95 100644 --- a/out/jl2py/csrc/jl2py.h +++ b/out/jl2py/csrc/jl2py.h @@ -136,11 +136,14 @@ jl_value_t *jl2py_box_float64(double x); jl_value_t *jl2py_box_string(const char *s, size_t len); jl_value_t *jl2py_box_symbol(const char *s); jl_value_t *jl2py_box_nothing(void); +jl_value_t *jl2py_box_complex128(double re, double im); int8_t jl2py_unbox_bool(jl_value_t *v); int64_t jl2py_unbox_int64(jl_value_t *v); double jl2py_unbox_float64(jl_value_t *v); const char *jl2py_unbox_string(jl_value_t *v, size_t *len_out); +/* Unbox a scalar Complex{Float32|Float64} into two doubles. Returns 1 on success. */ +int jl2py_unbox_complex128(jl_value_t *v, double *re_out, double *im_out); /* ── Reference management ────────────────────────────────── */ diff --git a/out/jl2py/csrc/jl2py_convert.c b/out/jl2py/csrc/jl2py_convert.c index 4394bb1..859035f 100644 --- a/out/jl2py/csrc/jl2py_convert.c +++ b/out/jl2py/csrc/jl2py_convert.c @@ -49,6 +49,20 @@ jl_value_t *jl2py_box_nothing(void) { return jl_nothing; } +/* Python complex -> Julia Complex{Float64}, via Base.complex(re, im). */ +jl_value_t *jl2py_box_complex128(double re, double im) { + if (jl2py_debug_flag) fprintf(stderr, "[jl2py] box_complex128: %f+%fim\n", re, im); + jl_value_t *fn = jl_get_global(jl_base_module, jl_symbol("complex")); + if (!fn) return NULL; + jl_value_t *re_v = NULL, *im_v = NULL, *res = NULL; + JL_GC_PUSH3(&re_v, &im_v, &res); + re_v = jl_box_float64(re); + im_v = jl_box_float64(im); + res = jl_call2(fn, re_v, im_v); + JL_GC_POP(); + return res; +} + /* ── Unboxing ────────────────────────────────────────────── */ int8_t jl2py_unbox_bool(jl_value_t *v) { @@ -76,6 +90,22 @@ const char *jl2py_unbox_string(jl_value_t *v, size_t *len_out) { return s; } +/* Unbox scalar Complex{Float32|Float64} -> two doubles (components promoted). */ +int jl2py_unbox_complex128(jl_value_t *v, double *re_out, double *im_out) { + if (!v || !re_out || !im_out) return 0; + jl_value_t *re = jl_get_nth_field(v, 0); + jl_value_t *im = jl_get_nth_field(v, 1); + if (!re || !im) return 0; + if (jl_typeis(re, jl_float32_type)) { + *re_out = (double)jl_unbox_float32(re); + *im_out = (double)jl_unbox_float32(im); + } else { + *re_out = jl_unbox_float64(re); + *im_out = jl_unbox_float64(im); + } + return 1; +} + /* ── Type inspection ─────────────────────────────────────── */ jl_value_t *jl2py_typeof(jl_value_t *v) { diff --git a/out/jl2py/csrc/jl2py_gc.c b/out/jl2py/csrc/jl2py_gc.c index 8c1becf..c906470 100644 --- a/out/jl2py/csrc/jl2py_gc.c +++ b/out/jl2py/csrc/jl2py_gc.c @@ -54,6 +54,8 @@ static jl_datatype_t *jl_string_type_cached = NULL; static jl_datatype_t *jl_symbol_type_cached = NULL; static jl_datatype_t *jl_module_type_cached = NULL; static jl_value_t *jl_missing_val_cached = NULL; +static jl_datatype_t *jl_complexf64_type_cached = NULL; +static jl_datatype_t *jl_complexf32_type_cached = NULL; uint32_t jl2py_compute_typetag(jl_value_t *v) { if (v == NULL) return JL2PY_TAG_UNKNOWN; @@ -74,6 +76,8 @@ uint32_t jl2py_compute_typetag(jl_value_t *v) { if (ty == jl_float16_type) return JL2PY_TAG_FLOAT16; if (ty == jl_float32_type) return JL2PY_TAG_FLOAT32; if (ty == jl_float64_type) return JL2PY_TAG_FLOAT64; + if (jl_complexf64_type_cached && ty == jl_complexf64_type_cached) return JL2PY_TAG_COMPLEX128; + if (jl_complexf32_type_cached && ty == jl_complexf32_type_cached) return JL2PY_TAG_COMPLEX64; if (ty == jl_char_type) return JL2PY_TAG_CHAR; if (ty == (jl_datatype_t *)jl_string_type_cached) return JL2PY_TAG_STRING; if (ty == (jl_datatype_t *)jl_symbol_type_cached) return JL2PY_TAG_SYMBOL; @@ -128,6 +132,17 @@ int jl2py_gc_init(void) { /* Try to get Missing singleton */ jl_missing_val_cached = jl_get_global(jl_base_module, jl_symbol("missing")); + /* Cache Complex{Float64}/{Float32} (interned, permanent) for scalar tagging */ + { + jl_value_t *complex_ua = jl_get_global(jl_base_module, jl_symbol("Complex")); + if (complex_ua) { + jl_complexf64_type_cached = + (jl_datatype_t *)jl_apply_type1(complex_ua, (jl_value_t *)jl_float64_type); + jl_complexf32_type_cached = + (jl_datatype_t *)jl_apply_type1(complex_ua, (jl_value_t *)jl_float32_type); + } + } + /* Register the GC root scanner callback */ jl_gc_set_cb_root_scanner(jl2py_gc_root_scanner, 1); diff --git a/out/jl2py/src/jl2py/_lib.py b/out/jl2py/src/jl2py/_lib.py index 669e92f..9fab2be 100644 --- a/out/jl2py/src/jl2py/_lib.py +++ b/out/jl2py/src/jl2py/_lib.py @@ -120,6 +120,8 @@ def _register_functions(lib): lib.jl2py_box_symbol.restype = VP lib.jl2py_box_nothing.argtypes = [] lib.jl2py_box_nothing.restype = VP + lib.jl2py_box_complex128.argtypes = [F64, F64] + lib.jl2py_box_complex128.restype = VP # Unboxing lib.jl2py_unbox_bool.argtypes = [VP] @@ -130,6 +132,8 @@ def _register_functions(lib): lib.jl2py_unbox_float64.restype = F64 lib.jl2py_unbox_string.argtypes = [VP, ctypes.POINTER(SZ)] lib.jl2py_unbox_string.restype = CP + lib.jl2py_unbox_complex128.argtypes = [VP, ctypes.POINTER(F64), ctypes.POINTER(F64)] + lib.jl2py_unbox_complex128.restype = ctypes.c_int # Ref management lib.jl2py_ref_create.argtypes = [VP] diff --git a/out/jl2py/src/jl2py/convert.py b/out/jl2py/src/jl2py/convert.py index 018fa69..c791ed1 100644 --- a/out/jl2py/src/jl2py/convert.py +++ b/out/jl2py/src/jl2py/convert.py @@ -61,6 +61,8 @@ def box_python_value(val): return lib.jl2py_box_int64(val) if isinstance(val, float): return lib.jl2py_box_float64(val) + if isinstance(val, complex): + return lib.jl2py_box_complex128(val.real, val.imag) if isinstance(val, str): encoded = val.encode("utf-8") return lib.jl2py_box_string(encoded, len(encoded)) diff --git a/out/jl2py/src/jl2py/types.py b/out/jl2py/src/jl2py/types.py index 1e7f451..29b93b6 100644 --- a/out/jl2py/src/jl2py/types.py +++ b/out/jl2py/src/jl2py/types.py @@ -18,6 +18,7 @@ TAG_NOTHING, TAG_MISSING, TAG_MODULE, TAG_DATATYPE, TAG_FUNCTION, TAG_INT8, TAG_UINT8, TAG_INT16, TAG_UINT16, TAG_INT32, TAG_UINT32, TAG_UINT64, TAG_FLOAT32, TAG_FLOAT16, TAG_CHAR, TAG_OTHER, + TAG_COMPLEX64, TAG_COMPLEX128, ) @@ -520,6 +521,29 @@ class JuliaFloat16(_NumericMixin, JuliaValGC): def __float__(self): return float(get_lib().jl2py_unbox_float64(self.ptr)) +class JuliaComplex128(JuliaValGC): + """Julia Complex{Float64} — behaves like a Python complex.""" + def _parts(self): + re = ctypes.c_double(); im = ctypes.c_double() + get_lib().jl2py_unbox_complex128(self.ptr, ctypes.byref(re), ctypes.byref(im)) + return re.value, im.value + + @property + def real(self): return self._parts()[0] + + @property + def imag(self): return self._parts()[1] + + def __complex__(self): + re, im = self._parts(); return complex(re, im) + + def __repr__(self): return repr(self.__complex__()) + + +class JuliaComplex64(JuliaComplex128): + """Julia Complex{Float32} — components promoted to Python float.""" + + # ── Typetag → wrapper class map ────────────────────────────── TYPETAG_MAP = { @@ -542,6 +566,8 @@ def __float__(self): return float(get_lib().jl2py_unbox_float64(self.ptr)) TAG_MODULE: JuliaModule, TAG_DATATYPE: JuliaType, TAG_FUNCTION: JuliaFunction, + TAG_COMPLEX64: JuliaComplex64, + TAG_COMPLEX128: JuliaComplex128, # Containers (TAG_ARRAY, TAG_TUPLE, TAG_DICT, etc.) are in containers.py } diff --git a/out/jl2py/tests/test_complex.py b/out/jl2py/tests/test_complex.py new file mode 100644 index 0000000..c15fadb --- /dev/null +++ b/out/jl2py/tests/test_complex.py @@ -0,0 +1,46 @@ +"""Regression tests for scalar Complex support. + + Python complex -> Julia Complex{Float64} (convert.box_python_value -> jl2py_box_complex128) + scalar Complex{Float64} -> JuliaComplex128 (compute_typetag + TYPETAG_MAP) + scalar Complex{Float32} -> JuliaComplex64 (components promoted to float) + complex(z), z.real/.imag via jl2py_unbox_complex128 + +Run: + JULIA_BINDIR=/bin python tests/test_complex.py +""" +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + +import jl2py # noqa: F401 (kept for parity with sibling test modules) + + +def test_complex_input(jl): + """Python complex is auto-boxed to Julia Complex{Float64} for dispatch.""" + double = jl.eval("z -> 2z") + r = double(complex(1, 2)) + assert complex(r) == complex(2, 4) + print("[PASS] complex input: 2*(1+2im) =", complex(r)) + + +def test_complex_output_scalar(jl): + """A scalar Complex{Float64} result wraps as JuliaComplex128 (Python complex protocol).""" + z = jl.eval("1.5 + 2.5im") + assert type(z).__name__ == "JuliaComplex128", type(z).__name__ + assert complex(z) == complex(1.5, 2.5) + assert z.real == 1.5 and z.imag == 2.5 + print("[PASS] scalar ComplexF64 -> JuliaComplex128:", complex(z)) + + +def test_complex32(jl): + """Complex{Float32} wraps as JuliaComplex64 with components promoted to float.""" + z = jl.eval("ComplexF32(3, -4)") + assert type(z).__name__ == "JuliaComplex64", type(z).__name__ + assert complex(z) == complex(3, -4) + print("[PASS] ComplexF32 -> JuliaComplex64:", complex(z)) + + +def test_complex_kwarg_value(jl): + """A complex passed as a keyword-arg value is boxed like a positional one.""" + g = jl.eval("f(x; w=1) = x*w") + assert complex(g(complex(1, 0), w=complex(0, 1))) == complex(0, 1) + print("[PASS] complex as a keyword-arg value")