Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions out/jl2py/csrc/jl2py.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────── */

Expand Down
30 changes: 30 additions & 0 deletions out/jl2py/csrc/jl2py_convert.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
15 changes: 15 additions & 0 deletions out/jl2py/csrc/jl2py_gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
4 changes: 4 additions & 0 deletions out/jl2py/src/jl2py/_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions out/jl2py/src/jl2py/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
26 changes: 26 additions & 0 deletions out/jl2py/src/jl2py/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)


Expand Down Expand Up @@ -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 = {
Expand All @@ -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
}

Expand Down
46 changes: 46 additions & 0 deletions out/jl2py/tests/test_complex.py
Original file line number Diff line number Diff line change
@@ -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=<julia>/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")
Loading