diff --git a/spy/backend/c/context.py b/spy/backend/c/context.py index b1f200a20..7ab85ade0 100644 --- a/spy/backend/c/context.py +++ b/spy/backend/c/context.py @@ -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") diff --git a/spy/backend/c/cwriter.py b/spy/backend/c/cwriter.py index fd6b8176c..b73188b45 100644 --- a/spy/backend/c/cwriter.py +++ b/spy/backend/c/cwriter.py @@ -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: @@ -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"): "*", @@ -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"): "-", } diff --git a/spy/backend/interp.py b/spy/backend/interp.py index 812eecfb4..d62827642 100644 --- a/spy/backend/interp.py +++ b/spy/backend/interp.py @@ -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) diff --git a/spy/doppler.py b/spy/doppler.py index 1a95e94c3..90c3dd636 100644 --- a/spy/doppler.py +++ b/spy/doppler.py @@ -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, @@ -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" diff --git a/spy/libspy/include/spy/operator.h b/spy/libspy/include/spy/operator.h index f4994132d..ef23002c2 100644 --- a/spy/libspy/include/spy/operator.h +++ b/spy/libspy/include/spy/operator.h @@ -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 @@ -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) @@ -43,6 +55,8 @@ DEFINE_CONV(f64, f32) #undef u8 #undef i32 #undef u32 +#undef i64 +#undef u64 #undef f32 #undef f64 @@ -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) \ + 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) { diff --git a/spy/libspy/include/spy/str.h b/spy/libspy/include/spy/str.h index 5994f19c6..cc57d399a 100644 --- a/spy/libspy/include/spy/str.h +++ b/spy/libspy/include/spy/str.h @@ -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); @@ -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); diff --git a/spy/libspy/src/str.c b/spy/libspy/src/str.c index 716d9b63c..44f2fda78 100644 --- a/spy/libspy/src/str.c +++ b/spy/libspy/src/str.c @@ -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); @@ -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); diff --git a/spy/tests/compiler/test_int.py b/spy/tests/compiler/test_int.py index 6e142c7ba..208f2ce16 100644 --- a/spy/tests/compiler/test_int.py +++ b/spy/tests/compiler/test_int.py @@ -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 @@ -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""" @@ -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 diff --git a/spy/tests/compiler/unsafe/test_sizeof.py b/spy/tests/compiler/unsafe/test_sizeof.py new file mode 100644 index 000000000..008c68829 --- /dev/null +++ b/spy/tests/compiler/unsafe/test_sizeof.py @@ -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 diff --git a/spy/tests/wasm_wrapper.py b/spy/tests/wasm_wrapper.py index 70aaa6738..3d283fc67 100644 --- a/spy/tests/wasm_wrapper.py +++ b/spy/tests/wasm_wrapper.py @@ -88,7 +88,16 @@ def __init__( self.w_functype = w_functype def py2wasm(self, pyval: Any, w_T: W_Type) -> Any: - if w_T in (B.w_i32, B.w_u32, B.w_i8, B.w_u8, B.w_f64, B.w_bool): + if w_T in ( + B.w_i32, + B.w_u32, + B.w_i8, + B.w_u8, + B.w_i64, + B.w_u64, + B.w_f64, + B.w_bool, + ): return pyval elif w_T is B.w_complex128: return (pyval.real, pyval.imag) @@ -134,7 +143,16 @@ def to_py_result(self, w_T: W_Type, res: Any) -> Any: if w_T is TYPES.w_NoneType: assert res is None return None - elif w_T in (B.w_i8, B.w_u8, B.w_i32, B.w_u32, B.w_f64, B.w_f32): + elif w_T in ( + B.w_i8, + B.w_u8, + B.w_i32, + B.w_u32, + B.w_i64, + B.w_u64, + B.w_f64, + B.w_f32, + ): return res elif w_T is B.w_complex128: real, imag = res diff --git a/spy/vm/astframe.py b/spy/vm/astframe.py index 1735358c2..44df50776 100644 --- a/spy/vm/astframe.py +++ b/spy/vm/astframe.py @@ -20,7 +20,7 @@ from spy.vm.object import W_Object, W_Type from spy.vm.opimpl import W_OpImpl from spy.vm.opspec import W_MetaArg -from spy.vm.primitive import W_Bool +from spy.vm.primitive import W_I64, W_U32, W_U64, W_Bool from spy.vm.struct import W_StructType from spy.vm.typechecker import maybe_plural @@ -207,6 +207,16 @@ def typecheck_maybe( def eval_expr(self, expr: ast.Expr, *, varname: Optional[str] = None) -> W_MetaArg: assert not self.redshifting, "DopplerFrame should override eval_expr" + # Integer-literal retyping: an int literal assigned to a variable with a + # known wider/other int type (e.g. `x: i64 = 5000000000`) must be wrapped + # directly to that type, NOT defaulted to i32 and then converted — the + # default would truncate before the conversion runs. We handle it here + # (rather than in eval_expr_Literal) because only the caller knows the + # target varname/type. + if isinstance(expr, ast.Literal) and type(expr.value) is int: + w_int = self._wrap_int_literal_maybe(expr, varname) + if w_int is not None: + return w_int wam = magic_dispatch(self, "eval_expr", expr) w_typeconv_opimpl = self.typecheck_maybe(wam, varname) @@ -891,6 +901,32 @@ def eval_expr_Const(self, const: ast.Const) -> W_MetaArg: assert const.w_T is not None return W_MetaArg(self.vm, "blue", const.w_T, const.w_val, const.loc) + # Map an int target type -> the W_* class used to wrap the literal without + # truncation. Only types whose range differs from i32's matter here. + _INT_LITERAL_WRAPPERS = { + B.w_i64: W_I64, + B.w_u64: W_U64, + B.w_u32: W_U32, + } + + def _wrap_int_literal_maybe( + self, const: ast.Literal, varname: Optional[str] + ) -> Optional[W_MetaArg]: + """ + If `const` (an int literal) is being assigned to a local with a declared + int type that isn't i32, wrap it directly to that type. Returns None to + fall back to the default (i32) handling. + """ + if varname is None or varname not in self.locals: + return None + w_T = self.locals[varname].w_T + pyclass = self._INT_LITERAL_WRAPPERS.get(w_T) + if pyclass is None: + return None + assert isinstance(const.value, int) + w_val = pyclass(const.value) + return W_MetaArg(self.vm, "blue", w_T, w_val, const.loc) + def eval_expr_Literal(self, const: ast.Literal) -> W_MetaArg: # unsupported literals are rejected directly by the parser, see # Parser.from_py_expr_Literal diff --git a/spy/vm/modules/operator/binop.py b/spy/vm/modules/operator/binop.py index 389cf00c3..eba2bb698 100644 --- a/spy/vm/modules/operator/binop.py +++ b/spy/vm/modules/operator/binop.py @@ -91,6 +91,18 @@ MM.register(">" , "i32", "i32", OP.w_i32_gt) MM.register(">=", "i32", "i32", OP.w_i32_ge) +# i64 / u64 ops (same shape as i32/u32; generated to avoid 32 hand lines each) +for _T in ("i64", "u64"): + for _op in ("+", "-", "*", "/", "//", "%", "<<", ">>", "&", "|", "^", + "==", "!=", "<", "<=", ">", ">="): + _name = { + "+": "add", "-": "sub", "*": "mul", "/": "div", "//": "floordiv", + "%": "mod", "<<": "lshift", ">>": "rshift", "&": "and", "|": "or", + "^": "xor", "==": "eq", "!=": "ne", "<": "lt", "<=": "le", + ">": "gt", ">=": "ge", + }[_op] + MM.register(_op, _T, _T, getattr(OP, f"w_{_T}_{_name}")) + # f64 ops MM.register("+", "f64", "f64", OP.w_f64_add) MM.register("-", "f64", "f64", OP.w_f64_sub) @@ -108,7 +120,7 @@ # mixed int/f64 ops: this is still small enough that we can write it manually, # but we should consider the idea of generating this table automatically. This # will become especially relevant when we add more integer types. -for num_t in ("i8", "u8", "u32", "i32", "f32"): +for num_t in ("i8", "u8", "u32", "i32", "i64", "u64", "f32"): MM.register("+", "f64", num_t, OP.w_f64_add) MM.register("+", num_t, "f64", OP.w_f64_add) MM.register("-", "f64", num_t, OP.w_f64_sub) diff --git a/spy/vm/modules/operator/convop.py b/spy/vm/modules/operator/convop.py index 18638d53e..290492146 100644 --- a/spy/vm/modules/operator/convop.py +++ b/spy/vm/modules/operator/convop.py @@ -13,8 +13,10 @@ W_F64, W_I8, W_I32, + W_I64, W_U8, W_U32, + W_U64, W_Bool, W_Complex128, W_Dynamic, @@ -188,6 +190,47 @@ def w_u32_to_i32(vm: "SPyVM", w_x: W_U32) -> W_I32: return W_I32(w_x.value) +# ---- 64-bit int conversions ---- +@OP.builtin_func +def w_i32_to_i64(vm: "SPyVM", w_x: W_I32) -> W_I64: + return W_I64(w_x.value) + + +@OP.builtin_func +def w_i32_to_u64(vm: "SPyVM", w_x: W_I32) -> W_U64: + return W_U64(w_x.value) + + +@OP.builtin_func +def w_i64_to_i32(vm: "SPyVM", w_x: W_I64) -> W_I32: + return W_I32(w_x.value) + + +@OP.builtin_func +def w_u64_to_i32(vm: "SPyVM", w_x: W_U64) -> W_I32: + return W_I32(w_x.value) + + +@OP.builtin_func +def w_i64_to_u64(vm: "SPyVM", w_x: W_I64) -> W_U64: + return W_U64(w_x.value) + + +@OP.builtin_func +def w_u64_to_i64(vm: "SPyVM", w_x: W_U64) -> W_I64: + return W_I64(w_x.value) + + +@OP.builtin_func +def w_i64_to_f64(vm: "SPyVM", w_x: W_I64) -> W_F64: + return vm.wrap(float(vm.unwrap_i64(w_x))) + + +@OP.builtin_func +def w_u64_to_f64(vm: "SPyVM", w_x: W_U64) -> W_F64: + return vm.wrap(float(vm.unwrap_u64(w_x))) + + @OP.builtin_func def w_f64_to_i32(vm: "SPyVM", w_x: W_F64) -> W_I32: i32_MIN, i32_MAX = -(2**31) - 1, 2**31 - 1 @@ -291,3 +334,12 @@ def w_from_dynamic_T(vm: "SPyVM", w_obj: W_Dynamic) -> T: MM.register("convert", "u8", "i32", OP.w_u8_to_i32) MM.register("convert", "i32", "u32", OP.w_i32_to_u32) MM.register("convert", "u32", "i32", OP.w_u32_to_i32) +# 64-bit: widening from i32 is implicit; cross-64 and narrowing are explicit. +MM.register("convert", "i32", "i64", OP.w_i32_to_i64) +MM.register("convert", "i32", "u64", OP.w_i32_to_u64) +MM.register("convert", "i64", "i32", OP.w_i64_to_i32) +MM.register("convert", "u64", "i32", OP.w_u64_to_i32) +MM.register("convert", "i64", "u64", OP.w_i64_to_u64) +MM.register("convert", "u64", "i64", OP.w_u64_to_i64) +MM.register("convert", "i64", "f64", OP.w_i64_to_f64) +MM.register("convert", "u64", "f64", OP.w_u64_to_f64) diff --git a/spy/vm/modules/operator/opimpl_int.py b/spy/vm/modules/operator/opimpl_int.py index 33f29341c..5101cc23f 100644 --- a/spy/vm/modules/operator/opimpl_int.py +++ b/spy/vm/modules/operator/opimpl_int.py @@ -2,7 +2,7 @@ from spy.errors import SPyError from spy.vm.object import W_Object -from spy.vm.primitive import W_F64, W_I8, W_I32, W_U8, W_U32, W_Bool +from spy.vm.primitive import W_F64, W_I8, W_I32, W_I64, W_U8, W_U32, W_U64, W_Bool from . import OP @@ -117,5 +117,7 @@ def w_neg(vm: "SPyVM", w_a: WT) -> WT: make_ops("i32", W_I32) make_ops("u32", W_U32) +make_ops("i64", W_I64) +make_ops("u64", W_U64) make_ops("i8", W_I8) make_ops("u8", W_U8) diff --git a/spy/vm/modules/operator/opimpl_str.py b/spy/vm/modules/operator/opimpl_str.py index 7cacbdab8..d68094b97 100644 --- a/spy/vm/modules/operator/opimpl_str.py +++ b/spy/vm/modules/operator/opimpl_str.py @@ -1,7 +1,16 @@ from typing import TYPE_CHECKING from spy.errors import SPyError -from spy.vm.primitive import W_I8, W_I32, W_U8, W_U32, W_Bool, W_Complex128 +from spy.vm.primitive import ( + W_I8, + W_I32, + W_I64, + W_U8, + W_U32, + W_U64, + W_Bool, + W_Complex128, +) from spy.vm.str import W_Str from . import OP @@ -55,6 +64,20 @@ def w_str_to_u32(vm: "SPyVM", w_s: W_Str) -> W_U32: return W_U32(val) +@OP.builtin_func +def w_str_to_i64(vm: "SPyVM", w_s: W_Str) -> W_I64: + val = _parse_int(vm, w_s) + _check_range(val, -(2**63), 2**63 - 1, "i64") + return W_I64(val) + + +@OP.builtin_func +def w_str_to_u64(vm: "SPyVM", w_s: W_Str) -> W_U64: + val = _parse_int(vm, w_s) + _check_range(val, 0, 2**64 - 1, "u64") + return W_U64(val) + + @OP.builtin_func def w_str_to_i8(vm: "SPyVM", w_s: W_Str) -> W_I8: val = _parse_int(vm, w_s) diff --git a/spy/vm/modules/operator/unaryop.py b/spy/vm/modules/operator/unaryop.py index 8a4e93f61..2ed7e6bcf 100644 --- a/spy/vm/modules/operator/unaryop.py +++ b/spy/vm/modules/operator/unaryop.py @@ -43,6 +43,7 @@ def w_NOT(vm: "SPyVM", wam_v: W_MetaArg) -> W_OpImpl: MM.register("-", "i8", None, OP.w_i8_neg) MM.register("-", "i32", None, OP.w_i32_neg) +MM.register("-", "i64", None, OP.w_i64_neg) MM.register("-", "f64", None, OP.w_f64_neg) MM.register("-", "f32", None, OP.w_f32_neg) MM.register("-", "complex128", None, OP.w_complex128_neg) diff --git a/spy/vm/modules/unsafe/div.py b/spy/vm/modules/unsafe/div.py index d4c57a7f0..97edc70cd 100644 --- a/spy/vm/modules/unsafe/div.py +++ b/spy/vm/modules/unsafe/div.py @@ -4,7 +4,7 @@ from spy.vm.b import B from spy.vm.modules.operator.multimethod import MultiMethodTable from spy.vm.opspec import W_MetaArg -from spy.vm.primitive import W_F32, W_F64, W_I8, W_I32, W_U8, W_U32 +from spy.vm.primitive import W_F32, W_F64, W_I8, W_I32, W_I64, W_U8, W_U32, W_U64 from spy.vm.w import W_Object, W_OpSpec, W_Type from . import UNSAFE @@ -108,6 +108,8 @@ def w_unchecked_mod(vm: "SPyVM", w_a: WT, w_b: WT) -> WT: make_ops("u8", W_U8) make_ops("i32", W_I32) make_ops("u32", W_U32) +make_ops("i64", W_I64) +make_ops("u64", W_U64) @UNSAFE.builtin_func diff --git a/spy/vm/modules/unsafe/misc.py b/spy/vm/modules/unsafe/misc.py index d5ad5a5c1..e86d44498 100644 --- a/spy/vm/modules/unsafe/misc.py +++ b/spy/vm/modules/unsafe/misc.py @@ -10,9 +10,9 @@ def sizeof(w_T: W_Type) -> int: if w_T in (B.w_i8, B.w_u8): return 1 - elif w_T is B.w_i32: + elif w_T in (B.w_i32, B.w_u32, B.w_f32): return 4 - elif w_T is B.w_f64: + elif w_T in (B.w_i64, B.w_u64, B.w_f64): return 8 elif isinstance(w_T, W_StructType): return w_T.size diff --git a/spy/vm/primitive.py b/spy/vm/primitive.py index e9b22dc01..66c6a8b14 100644 --- a/spy/vm/primitive.py +++ b/spy/vm/primitive.py @@ -147,6 +147,98 @@ def w_repr(vm: "SPyVM", w_self: "W_U32") -> "W_Str": return vm.wrap(str(i)) +@B.builtin_type("i64", lazy_definition=True) +class W_I64(W_Object): + __spy_storage_category__ = "value" + value: fixedint.Int64 + + def __init__(self, value: int | FixedInt) -> None: + self.value = fixedint.Int64(value) + + @builtin_method("__new__", color="blue", kind="metafunc") + @staticmethod + def w_NEW(vm: "SPyVM", wam_cls: "W_MetaArg", *args_wam: "W_MetaArg") -> "W_OpSpec": + from spy.vm.opspec import W_OpSpec + + if len(args_wam) != 1: + return W_OpSpec.NULL + wam_arg = args_wam[0] + if wam_arg.w_static_T == B.w_i32: + return W_OpSpec(OP.w_i32_to_i64, [wam_arg]) + elif wam_arg.w_static_T == B.w_u64: + return W_OpSpec(OP.w_u64_to_i64, [wam_arg]) + elif wam_arg.w_static_T == B.w_str: + return W_OpSpec(OP.w_str_to_i64, [wam_arg]) + return W_OpSpec.NULL + + def __repr__(self) -> str: + return f"W_I64({self.value})" + + def spy_unwrap(self, vm: "SPyVM") -> fixedint.Int64: + return self.value + + def spy_key(self, vm: "SPyVM") -> fixedint.Int64: + return self.value + + @builtin_method("__str__") + @staticmethod + def w_str(vm: "SPyVM", w_self: "W_I64") -> "W_Str": + i = vm.unwrap(w_self) + return vm.wrap(str(i)) + + @builtin_method("__repr__") + @staticmethod + def w_repr(vm: "SPyVM", w_self: "W_I64") -> "W_Str": + i = vm.unwrap(w_self) + return vm.wrap(str(i)) + + +@B.builtin_type("u64", lazy_definition=True) +class W_U64(W_Object): + __spy_storage_category__ = "value" + value: fixedint.UInt64 + + def __init__(self, value: int | FixedInt) -> None: + self.value = fixedint.UInt64(value) + + @builtin_method("__new__", color="blue", kind="metafunc") + @staticmethod + def w_NEW(vm: "SPyVM", wam_cls: "W_MetaArg", *args_wam: "W_MetaArg") -> "W_OpSpec": + from spy.vm.opspec import W_OpSpec + + if len(args_wam) != 1: + return W_OpSpec.NULL + wam_arg = args_wam[0] + if wam_arg.w_static_T == B.w_i32: + return W_OpSpec(OP.w_i32_to_u64, [wam_arg]) + elif wam_arg.w_static_T == B.w_i64: + return W_OpSpec(OP.w_i64_to_u64, [wam_arg]) + elif wam_arg.w_static_T == B.w_str: + return W_OpSpec(OP.w_str_to_u64, [wam_arg]) + return W_OpSpec.NULL + + def __repr__(self) -> str: + return f"W_U64({self.value})" + + def spy_unwrap(self, vm: "SPyVM") -> fixedint.UInt64: + return self.value + + def spy_key(self, vm: "SPyVM") -> fixedint.UInt64: + return self.value + + @builtin_method("__str__") + @staticmethod + def w_str(vm: "SPyVM", w_self: "W_U64") -> "W_Str": + u = vm.unwrap(w_self) + return vm.wrap(str(u)) + + @builtin_method("__repr__") + @staticmethod + def w_repr(vm: "SPyVM", w_self: "W_U64") -> "W_Str": + u = vm.unwrap(w_self) + return vm.wrap(str(u)) + + @B.builtin_type("i8", lazy_definition=True) class W_I8(W_Object): __spy_storage_category__ = "value" diff --git a/spy/vm/vm.py b/spy/vm/vm.py index bcde28bcf..18640ff14 100644 --- a/spy/vm/vm.py +++ b/spy/vm/vm.py @@ -51,8 +51,10 @@ W_F64, W_I8, W_I32, + W_I64, W_U8, W_U32, + W_U64, W_Bool, W_Complex128, W_Dynamic, @@ -77,6 +79,8 @@ W_FuncType._w.define(W_FuncType) W_I32._w.define(W_I32) W_U32._w.define(W_U32) +W_I64._w.define(W_I64) +W_U64._w.define(W_U64) W_I8._w.define(W_I8) W_U8._w.define(W_U8) W_F64._w.define(W_F64) @@ -631,6 +635,10 @@ def wrap(self, value: Any) -> W_Object: return W_I32(value) elif T is fixedint.UInt32: return W_U32(value) + elif T is fixedint.Int64: + return W_I64(value) + elif T is fixedint.UInt64: + return W_U64(value) elif T is fixedint.Int8: return W_I8(value) elif T is fixedint.UInt8: @@ -702,6 +710,16 @@ def unwrap_u32(self, w_value: W_Object) -> Any: raise Exception("Type mismatch") return w_value.value + def unwrap_i64(self, w_value: W_Object) -> Any: + if not isinstance(w_value, W_I64): + raise Exception("Type mismatch") + return w_value.value + + def unwrap_u64(self, w_value: W_Object) -> Any: + if not isinstance(w_value, W_U64): + raise Exception("Type mismatch") + return w_value.value + def unwrap_i8(self, w_value: W_Object) -> Any: if not isinstance(w_value, W_I8): raise Exception("Type mismatch")