Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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[128];
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[128];
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
16 changes: 16 additions & 0 deletions spy/tests/compiler/unsafe/test_sizeof.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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_f64, 8),
])
def test_sizeof_primitives(w_T, expected):
assert sizeof(w_T) == expected
Loading