Skip to content
Open
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
2 changes: 2 additions & 0 deletions spy/backend/c/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ def __init__(self, vm: SPyVM) -> None:
self._d[B.w_u8] = C_Type("uint8_t")
self._d[B.w_i32] = C_Type("int32_t")
self._d[B.w_u32] = C_Type("uint32_t")
self._d[B.w_i64] = C_Type("int64_t")
self._d[B.w_u64] = C_Type("uint64_t")
self._d[B.w_f64] = C_Type("double")
self._d[B.w_f32] = C_Type("float")
self._d[B.w_complex128] = C_Type("spy_Complex128")
Expand Down
37 changes: 36 additions & 1 deletion spy/backend/c/cwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,9 +277,13 @@ def fmt_expr_Const(self, const: ast.Const) -> C.Expr:
w_val = const.w_val
if w_T is B.w_bool:
return C.Literal(str(vm.unwrap_bool(w_val)).lower())
elif w_T in (B.w_i32, B.w_i8, B.w_u8):
elif w_T in (B.w_i32, B.w_i8, B.w_u8, B.w_u32):
intval = int(vm.unwrap(w_val))
return C.Literal(str(intval))
elif w_T is B.w_i64:
return C.Literal(str(int(vm.unwrap(w_val))) + "LL")
elif w_T is B.w_u64:
return C.Literal(str(int(vm.unwrap(w_val))) + "ULL")
elif w_T is B.w_f64:
return C.Literal(str(vm.unwrap_f64(w_val)))
elif w_T is B.w_complex128:
Expand Down Expand Up @@ -474,6 +478,36 @@ def fmt_expr_Or(self, op: ast.Or) -> C.Expr:
FQN("operator::u32_gt"): ">",
FQN("operator::u32_ge"): ">=",
#
FQN("operator::i64_add"): "+",
FQN("operator::i64_sub"): "-",
FQN("operator::i64_mul"): "*",
FQN("operator::i64_lshift"): "<<",
FQN("operator::i64_rshift"): ">>",
FQN("operator::i64_and"): "&",
FQN("operator::i64_or"): "|",
FQN("operator::i64_xor"): "^",
FQN("operator::i64_eq"): "==",
FQN("operator::i64_ne"): "!=",
FQN("operator::i64_lt"): "<",
FQN("operator::i64_le"): "<=",
FQN("operator::i64_gt"): ">",
FQN("operator::i64_ge"): ">=",
#
FQN("operator::u64_add"): "+",
FQN("operator::u64_sub"): "-",
FQN("operator::u64_mul"): "*",
FQN("operator::u64_lshift"): "<<",
FQN("operator::u64_rshift"): ">>",
FQN("operator::u64_and"): "&",
FQN("operator::u64_or"): "|",
FQN("operator::u64_xor"): "^",
FQN("operator::u64_eq"): "==",
FQN("operator::u64_ne"): "!=",
FQN("operator::u64_lt"): "<",
FQN("operator::u64_le"): "<=",
FQN("operator::u64_gt"): ">",
FQN("operator::u64_ge"): ">=",
#
FQN("operator::f64_add"): "+",
FQN("operator::f64_sub"): "-",
FQN("operator::f64_mul"): "*",
Expand All @@ -499,6 +533,7 @@ def fmt_expr_Or(self, op: ast.Or) -> C.Expr:
FQN2UnaryOp = {
FQN("operator::i8_neg"): "-",
FQN("operator::i32_neg"): "-",
FQN("operator::i64_neg"): "-",
FQN("operator::f64_neg"): "-",
}

Expand Down
4 changes: 4 additions & 0 deletions spy/backend/interp.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ def __call__(self, *args: Any, unwrap: bool = True) -> Any:
arg = fixedint.UInt8(arg)
elif w_T is B.w_u32:
arg = fixedint.UInt32(arg)
elif w_T is B.w_i64:
arg = fixedint.Int64(arg)
elif w_T is B.w_u64:
arg = fixedint.UInt64(arg)
elif w_T is B.w_f32:
arg = float32(arg)
w_arg = self.vm.wrap(arg)
Expand Down
14 changes: 13 additions & 1 deletion spy/doppler.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def make_const(vm: "SPyVM", loc: Loc, w_val: W_Object) -> ast.Expr:
B.w_i8,
B.w_u8,
B.w_u32,
B.w_i64,
B.w_u64,
B.w_f64,
B.w_complex128,
B.w_bool,
Expand Down Expand Up @@ -390,7 +392,17 @@ def eval_expr(
-> compute shited binop (stored in .shifted_expr)
"""
assert self.redshifting
wam = magic_dispatch(self, "eval_expr", expr)
# Same int-literal retyping as ASTFrame.eval_expr (see the comment there):
# an int literal bound to a non-i32 int local must be wrapped directly to
# that type, not defaulted to i32 and truncated. The retyped wam is blue,
# so shift_expr emits a correctly-typed Const for it.
wam_int = None
if isinstance(expr, ast.Literal) and type(expr.value) is int:
wam_int = self._wrap_int_literal_maybe(expr, varname)
if wam_int is not None:
wam = wam_int
else:
wam = magic_dispatch(self, "eval_expr", expr)
new_expr = self.shift_expr(expr, wam)
assert new_expr.w_T is not None, "shift_expr should return a typed ast.Expr"

Expand Down
60 changes: 60 additions & 0 deletions spy/libspy/include/spy/operator.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
#define u8 uint8_t
#define i32 int32_t
#define u32 uint32_t
#define i64 int64_t
#define u64 uint64_t
#define f32 float
#define f64 double

Expand All @@ -35,6 +37,16 @@ DEFINE_CONV(u8, f64)
DEFINE_CONV(u32, i32)
DEFINE_CONV(u32, f64)

// 64-bit int conversions (see convop.py registrations).
DEFINE_CONV(i32, i64)
DEFINE_CONV(i32, u64)
DEFINE_CONV(i64, i32)
DEFINE_CONV(u64, i32)
DEFINE_CONV(i64, u64)
DEFINE_CONV(u64, i64)
DEFINE_CONV(i64, f64)
DEFINE_CONV(u64, f64)

DEFINE_CONV(f32, f64)

DEFINE_CONV(f64, f32)
Expand All @@ -43,6 +55,8 @@ DEFINE_CONV(f64, f32)
#undef u8
#undef i32
#undef u32
#undef i64
#undef u64
#undef f32
#undef f64

Expand Down Expand Up @@ -372,6 +386,52 @@ spy_unsafe$u32_unchecked_mod(uint32_t x, uint32_t y) {
return x % y;
}

// ---- 64-bit int div / floordiv / mod ----
// Generated for {i64, u64} via a macro to avoid ~12 near-identical functions
// each. `div` returns double (Python `/` semantics, matching i32/u32);
// floordiv/mod return the integer type. SIGNED guards the (x^y)<0 floor
// correction, which is meaningless for unsigned.
// NAME is the SPy type name used in the symbol (i64/u64); CT is the C type.
#define SPY_DEF_INT64_DIVMOD(NAME, CT, SIGNED) \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not necessarily opposed to this but:

  1. if we do this, we should do for ALL div/mod/floordiv functions, not just the i64 ones
  2. the full name of the function should be spelled explicitly in the macro: this makes it much more easy to grep. Something like:
DEFINE_DIV(spy_operator$i64_div, i64, int64_t)
DEFINE_DIV(spy_operator$i32_div, i32, int32_t)
...
DEFINE_UNCHECKED_DIV(spy_operator$i64_unchecked_div, i64, int64_t)
...

there is more code duplication than with this approach, but also much less magic IMHO.

static inline double spy_operator$##NAME##_div(CT x, CT y) { \
if (y == 0) \
spy_panic("ZeroDivisionError", "division by zero", __FILE__, __LINE__); \
return (double)x / y; \
} \
static inline double spy_unsafe$##NAME##_unchecked_div(CT x, CT y) { \
return (double)x / y; \
} \
static inline CT spy_operator$##NAME##_floordiv(CT x, CT y) { \
if (y == 0) \
spy_panic("ZeroDivisionError", "integer division or modulo by zero", \
__FILE__, __LINE__); \
CT q = x / y; \
if (SIGNED) { CT r = x % y; if (r != 0 && ((x ^ y) < 0)) q -= 1; } \
return q; \
} \
static inline CT spy_unsafe$##NAME##_unchecked_floordiv(CT x, CT y) { \
CT q = x / y; \
if (SIGNED) { CT r = x % y; if (r != 0 && ((x ^ y) < 0)) q -= 1; } \
return q; \
} \
static inline CT spy_operator$##NAME##_mod(CT x, CT y) { \
if (y == 0) \
spy_panic("ZeroDivisionError", "integer modulo by zero", __FILE__, \
__LINE__); \
CT r = x % y; \
if (SIGNED) { if (r != 0 && ((x ^ y) < 0)) r += y; } \
return r; \
} \
static inline CT spy_unsafe$##NAME##_unchecked_mod(CT x, CT y) { \
CT r = x % y; \
if (SIGNED) { if (r != 0 && ((x ^ y) < 0)) r += y; } \
return r; \
}

SPY_DEF_INT64_DIVMOD(i64, int64_t, 1)
SPY_DEF_INT64_DIVMOD(u64, uint64_t, 0)
#undef SPY_DEF_INT64_DIVMOD

static inline double
spy_operator$f64_div(double x, double y) {
if (y == 0) {
Expand Down
8 changes: 8 additions & 0 deletions spy/libspy/include/spy/str.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ spy_str_identity(spy_StrObject *s) {
// __str__ methods of common builtin types
spy_StrObject *spy_builtins$i32$__str__(int32_t x);

spy_StrObject *spy_builtins$i64$__str__(int64_t x);

spy_StrObject *spy_builtins$u64$__str__(uint64_t x);

spy_StrObject *spy_builtins$i8$__str__(int8_t x);

spy_StrObject *spy_builtins$u8$__str__(uint8_t x);
Expand All @@ -131,6 +135,10 @@ int32_t spy_operator$str_to_i32(spy_StrObject *s);

uint32_t spy_operator$str_to_u32(spy_StrObject *s);

int64_t spy_operator$str_to_i64(spy_StrObject *s);

uint64_t spy_operator$str_to_u64(spy_StrObject *s);

int8_t spy_operator$str_to_i8(spy_StrObject *s);

uint8_t spy_operator$str_to_u8(spy_StrObject *s);
Expand Down
79 changes: 79 additions & 0 deletions spy/libspy/src/str.c
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ spy_builtins$i32$__str__(int32_t x) {
return spy_str_from_format("%d", x);
}

spy_StrObject *
spy_builtins$i64$__str__(int64_t x) {
return spy_str_from_format("%lld", (long long)x);
}

spy_StrObject *
spy_builtins$u64$__str__(uint64_t x) {
return spy_str_from_format("%llu", (unsigned long long)x);
}

spy_StrObject *
spy_builtins$i8$__str__(int8_t x) {
return spy_str_from_format("%d", (int)x);
Expand Down Expand Up @@ -150,6 +160,75 @@ spy_operator$str_to_u32(spy_StrObject *s) {
return (uint32_t)val;
}

int64_t
spy_operator$str_to_i64(spy_StrObject *s) {
// We can't reuse spy_str_parse_i64: it reports overflow as a ValueError,
// but for an explicit i64() conversion an out-of-range value must be an
// OverflowError (matching the interp path and the other int types).
char buf[64];
size_t len = s->length;
if (len >= sizeof(buf)) {
spy_panic(
"ValueError", "invalid literal for int() with base 10", __FILE__, __LINE__
);
}
memcpy(buf, spy_StrObject_UTF8(s), len);
buf[len] = '\0';

char *end;
errno = 0;
long long val = strtoll(buf, &end, 10);
if (end == buf || *end != '\0') {
char msg[128];
snprintf(msg, sizeof(msg), "invalid literal for int() with base 10: '%s'", buf);
spy_panic("ValueError", msg, __FILE__, __LINE__);
}
if (errno != 0) {
char msg[256];
snprintf(
msg, sizeof(msg),
"i64 value %s out of range [-9223372036854775808, 9223372036854775807]",
buf
);
spy_panic("OverflowError", msg, __FILE__, __LINE__);
}
return (int64_t)val;
}

uint64_t
spy_operator$str_to_u64(spy_StrObject *s) {
// u64's max exceeds int64, so we can't reuse spy_str_parse_i64. strtoull
// would silently wrap a leading '-' into a huge positive value, so we
// reject negatives explicitly to match Python's int() + range semantics.
char buf[64];
size_t len = s->length;
if (len >= sizeof(buf)) {
spy_panic(
"ValueError", "invalid literal for int() with base 10", __FILE__, __LINE__
);
}
memcpy(buf, spy_StrObject_UTF8(s), len);
buf[len] = '\0';

char *end;
errno = 0;
unsigned long long val = strtoull(buf, &end, 10);
if (end == buf || *end != '\0') {
char msg[128];
snprintf(msg, sizeof(msg), "invalid literal for int() with base 10: '%s'", buf);
spy_panic("ValueError", msg, __FILE__, __LINE__);
}
if (errno != 0 || strchr(buf, '-') != NULL) {
char msg[256];
snprintf(
msg, sizeof(msg),
"u64 value %s out of range [0, 18446744073709551615]", buf
);
spy_panic("OverflowError", msg, __FILE__, __LINE__);
}
return (uint64_t)val;
}

int8_t
spy_operator$str_to_i8(spy_StrObject *s) {
int64_t val = spy_str_parse_i64(s);
Expand Down
41 changes: 39 additions & 2 deletions spy/tests/compiler/test_int.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from spy.tests.support import CompilerTest


@pytest.fixture(params=["i32", "u32", "i8", "u8"])
@pytest.fixture(params=["i32", "u32", "i8", "u8", "i64", "u64"])
def int_type(request):
return request.param

Expand Down Expand Up @@ -126,7 +126,7 @@ def floordiv(x: T, y: T) -> T: return x // y
mod.floordiv(11, 0)

def test_division_mixed_signs(self, int_type):
if int_type in ("u8", "u32"):
if int_type in ("u8", "u32", "u64"):
pytest.skip("Skipping for negative operands in floordiv test")

mod = self.compile(f"""
Expand Down Expand Up @@ -239,9 +239,46 @@ def foo(s: str) -> T:
"u32": ("4294967296", "-1"),
"i8": ("128", "-129"),
"u8": ("256", "-1"),
"i64": ("9223372036854775808", "-9223372036854775809"),
"u64": ("18446744073709551616", "-1"),
}
too_big, too_small = limits[int_type]
with SPyError.raises("W_OverflowError", match="out of range"):
mod.foo(too_big)
with SPyError.raises("W_OverflowError", match="out of range"):
mod.foo(too_small)

def test_i64_large_literal(self):
# Values beyond i32 range must not be truncated when the variable is
# explicitly typed as i64/u64.
mod = self.compile("""
def get_i64() -> i64:
x: i64 = 5000000000
return x

def get_u64() -> u64:
x: u64 = 10000000000
return x
""")
assert mod.get_i64() == 5_000_000_000
assert mod.get_u64() == 10_000_000_000

def test_i64_u64_conversion(self):
mod = self.compile("""
def i32_to_i64(x: i32) -> i64: return x
def i64_to_i32(x: i64) -> i32: return x
def i32_to_u64(x: i32) -> u64: return x
def u64_to_i32(x: u64) -> i32: return x
def i64_to_u64(x: i64) -> u64: return x
def u64_to_i64(x: u64) -> i64: return x
def i64_to_f64(x: i64) -> f64: return x
def u64_to_f64(x: u64) -> f64: return x
""")
assert mod.i32_to_i64(42) == 42
assert mod.i64_to_i32(42) == 42
assert mod.i32_to_u64(42) == 42
assert mod.u64_to_i32(42) == 42
assert mod.i64_to_u64(42) == 42
assert mod.u64_to_i64(42) == 42
assert mod.i64_to_f64(42) == 42.0
assert mod.u64_to_f64(42) == 42.0
21 changes: 21 additions & 0 deletions spy/tests/compiler/unsafe/test_sizeof.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import pytest

from spy.vm.b import B
from spy.vm.modules.unsafe.misc import sizeof


@pytest.mark.parametrize(
"w_T, expected",
[
(B.w_i8, 1),
(B.w_u8, 1),
(B.w_i32, 4),
(B.w_u32, 4),
(B.w_f32, 4),
(B.w_i64, 8),
(B.w_u64, 8),
(B.w_f64, 8),
],
)
def test_sizeof_primitives(w_T, expected):
assert sizeof(w_T) == expected
Loading
Loading