diff --git a/pyre/extra_tests/snippets/stdlib_marshal.py b/pyre/extra_tests/snippets/stdlib_marshal.py index b3887f24b61..b09400416c3 100644 --- a/pyre/extra_tests/snippets/stdlib_marshal.py +++ b/pyre/extra_tests/snippets/stdlib_marshal.py @@ -99,6 +99,31 @@ def test_roundtrip(self): with self.assertRaises(ValueError): marshal.dumps([orig], allow_code=False) + def test_argument_binding(self): + data = marshal.dumps([1, 2, 3]) + + # `version` is positional-only: it binds positionally but rejects the + # keyword form. + self.assertEqual(marshal.loads(marshal.dumps([1, 2, 3], 2)), [1, 2, 3]) + with self.assertRaises(TypeError): + marshal.dumps([1], version=2) + # a non-integer version reaches int() and raises. + with self.assertRaises(TypeError): + marshal.dumps([1], None) + + # `bytes` / `file` are positional-only too. + with self.assertRaises(TypeError): + marshal.loads(bytes=data) + + # `allow_code` is keyword-only and truth-tested, so a falsy value + # (including None) rejects a nested code object. + code = compile("1 + 1", "", "eval") + dumped_code = marshal.dumps([code]) + with self.assertRaises(ValueError): + marshal.dumps([code], allow_code=None) + with self.assertRaises(ValueError): + marshal.loads(dumped_code, allow_code=False) + if __name__ == "__main__": unittest.main() diff --git a/pyre/extra_tests/snippets/stdlib_struct.py b/pyre/extra_tests/snippets/stdlib_struct.py index 2b8c6c9fbb3..42c394f1851 100644 --- a/pyre/extra_tests/snippets/stdlib_struct.py +++ b/pyre/extra_tests/snippets/stdlib_struct.py @@ -102,3 +102,19 @@ def __index__(self): unpack_iterator_type = type(struct.iter_unpack("B", b"")) with assert_raises(TypeError): unpack_iterator_type() + +# unpack_from accepts buffer / offset positionally or by keyword. +_buf = struct.pack("ii", 111, 222) +assert struct.unpack_from("ii", _buf, offset=0) == (111, 222) +assert struct.unpack_from("ii", buffer=_buf, offset=0) == (111, 222) +_s = struct.Struct("ii") +assert _s.unpack_from(_buf, offset=0) == (111, 222) +assert _s.unpack_from(buffer=_buf) == (111, 222) + +# pack / pack_into (module and method) reject keyword arguments. +with assert_raises(TypeError): + struct.pack(format="ii") +with assert_raises(TypeError): + struct.pack_into("ii", bytearray(8), 0, 1, 2, extra=3) +with assert_raises(TypeError): + _s.pack_into(bytearray(8), 0, 1, 2, extra=3) diff --git a/pyre/pyre-interpreter/src/call.rs b/pyre/pyre-interpreter/src/call.rs index df563971580..f1f2e768de9 100644 --- a/pyre/pyre-interpreter/src/call.rs +++ b/pyre/pyre-interpreter/src/call.rs @@ -918,10 +918,16 @@ pub(crate) fn set_orig_class( } } -fn finish_builtin_code_positional( +/// Invoke a builtin from a slice of raw positional arguments, binding through +/// its `Signature` first when it has a HOPELESS fast arity. `builtin_code_call` +/// itself never binds — the direct call sites hand it an already-bound flat +/// slice — so any entry that starts from raw positionals (the frame dispatch +/// here, and the JIT residual-call path in `pyre-jit`) must route through this +/// to give a `*args`/optional-positional body the slot shape it reads. +pub fn builtin_code_call_positional( current_code: PyObjectRef, current_args: &[PyObjectRef], -) -> PyResult { +) -> Result { if let Some(sig) = unsafe { crate::builtin_code_get_signature(current_code) } { // Every HOPELESS signature needs `_match_signature`, not only // *args/**kwargs/kw-only shapes. A plain optional positional @@ -941,7 +947,7 @@ fn finish_builtin_code_positional( fn call_builtin_code_many_from_roots(root_base: usize, nargs: usize) -> PyResult { let mut rooted = vec![pyre_object::PY_NULL; 1 + nargs]; pyre_object::gc_roots::shadow_stack_copy_range(root_base, &mut rooted); - finish_builtin_code_positional(rooted[0], &rooted[1..]) + builtin_code_call_positional(rooted[0], &rooted[1..]) } fn call_builtin_code_positional(code: PyObjectRef, args: &[PyObjectRef]) -> PyResult { @@ -963,28 +969,28 @@ fn call_builtin_code_positional(code: PyObjectRef, args: &[PyObjectRef]) -> PyRe // indirect call. The uncommon variadic case stays a residual helper. let current_code = _roots.get(root_base); match args.len() { - 0 => finish_builtin_code_positional(current_code, &[]), + 0 => builtin_code_call_positional(current_code, &[]), 1 => { let a0 = _roots.get(root_base + 1); - finish_builtin_code_positional(current_code, &[a0]) + builtin_code_call_positional(current_code, &[a0]) } 2 => { let a0 = _roots.get(root_base + 1); let a1 = _roots.get(root_base + 2); - finish_builtin_code_positional(current_code, &[a0, a1]) + builtin_code_call_positional(current_code, &[a0, a1]) } 3 => { let a0 = _roots.get(root_base + 1); let a1 = _roots.get(root_base + 2); let a2 = _roots.get(root_base + 3); - finish_builtin_code_positional(current_code, &[a0, a1, a2]) + builtin_code_call_positional(current_code, &[a0, a1, a2]) } 4 => { let a0 = _roots.get(root_base + 1); let a1 = _roots.get(root_base + 2); let a2 = _roots.get(root_base + 3); let a3 = _roots.get(root_base + 4); - finish_builtin_code_positional(current_code, &[a0, a1, a2, a3]) + builtin_code_call_positional(current_code, &[a0, a1, a2, a3]) } nargs => call_builtin_code_many_from_roots(root_base, nargs), } diff --git a/pyre/pyre-interpreter/src/module/_random/macro_smoke.rs b/pyre/pyre-interpreter/src/module/_random/macro_smoke.rs index 646e503ea28..6ada48cb084 100644 --- a/pyre/pyre-interpreter/src/module/_random/macro_smoke.rs +++ b/pyre/pyre-interpreter/src/module/_random/macro_smoke.rs @@ -109,6 +109,20 @@ fn _kwonly_bound_probe( value + adjustment } +/// A `#[posonly]` marker on the first non-positional-only parameter ends the +/// positional-only run before it: `base` is positional-only, so the derived +/// `Signature` carries `posonlyargcount == 1` and a keyword named `base` is +/// rejected by `raise_if_posonly_kwds`. +#[crate::pyre_function] +fn _posonly_bound_probe( + base: i64, + #[posonly] + #[default(0i64)] + offset: i64, +) -> i64 { + base + offset +} + crate::py_module! { "_pyre_smoke", interpleveldefs: { @@ -184,6 +198,36 @@ mod tests { assert_eq!(unsafe { w_int_get_value(result) }, 42); } + #[test] + fn posonly_marker_makes_leading_param_positional_only() { + crate::typedef::init_typeobjects(); + let signature = _posonly_bound_probe_pyre_sig().expect("derived signature"); + assert_eq!(signature.posonlyargcount, 1); + assert_eq!(signature.argnames, vec!["base", "offset"]); + + // The positional-only `base` binds fine by position, and `offset` + // still binds by keyword. + let bound = crate::call::bind_kwargs_to_signature( + &signature, + "_posonly_bound_probe", + &[w_int_new(40)], + &[(rustpython_wtf8::Wtf8Buf::from("offset"), w_int_new(2))], + ) + .expect("signature binding"); + let result = _posonly_bound_probe(&bound).expect("bound positional-only scope"); + assert_eq!(unsafe { w_int_get_value(result) }, 42); + + // Passing the positional-only `base` as a keyword is a TypeError. + let err = crate::call::bind_kwargs_to_signature( + &signature, + "_posonly_bound_probe", + &[], + &[(rustpython_wtf8::Wtf8Buf::from("base"), w_int_new(40))], + ) + .expect_err("positional-only name as keyword must error"); + assert_eq!(err.kind, crate::PyErrorKind::TypeError); + } + /// `Vec` return auto-wraps to a list. #[test] fn path_bytes_returns_list() { diff --git a/pyre/pyre-interpreter/src/module/binascii/mod.rs b/pyre/pyre-interpreter/src/module/binascii/mod.rs index 9152bc3c580..5f1ace2065d 100644 --- a/pyre/pyre-interpreter/src/module/binascii/mod.rs +++ b/pyre/pyre-interpreter/src/module/binascii/mod.rs @@ -13,99 +13,41 @@ mod transforms; use pyre_object::*; -/// `PyArg_UnpackTuple` for the entries that declare no keyword at all -/// (`crc32(data, crc=0, /)`, `crc_hqx(data, crc, /)`). A keyword is refused -/// under the module-qualified name; the arity message names the function bare -/// and is the only one in this module that carries no `()`. -fn unpack_positional<'a>( - args: &'a [PyObjectRef], +/// A required slot the `Signature` binder left `PY_NULL` because the caller +/// omitted it. `_PyArg` reports the bare parameter name and its 1-based +/// position. +fn arg_required( + w: PyObjectRef, fn_name: &str, - min: usize, - max: usize, -) -> Result<&'a [PyObjectRef], crate::PyError> { - let (pos, kwargs) = crate::builtins::split_builtin_kwargs(args); - if crate::builtins::real_kwarg_count(kwargs) != 0 { + param: &str, + pos: usize, +) -> Result { + if w.is_null() { return Err(crate::PyError::type_error(format!( - "binascii.{fn_name}() takes no keyword arguments" + "{fn_name}() missing required argument '{param}' (pos {pos})" ))); } - if pos.len() < min { - return Err(crate::PyError::type_error(format!( - "{fn_name} expected {}{min} argument{}, got {}", - if min == max { "" } else { "at least " }, - if min == 1 { "" } else { "s" }, - pos.len(), - ))); - } - if pos.len() > max { - return Err(crate::PyError::type_error(format!( - "{fn_name} expected {}{max} arguments, got {}", - if min == max { "" } else { "at most " }, - pos.len(), - ))); - } - Ok(pos) + Ok(w) } -/// The entries whose `data` is positional-only and whose remaining slots are -/// keyword-only (`a2b_base64`, `b2a_base64`, `b2a_uu`). A positional-only -/// parameter is never reported by name, so both an omitted and a surplus -/// positional read as the one positional slot; `_PyArg_UnpackKeywords` holds -/// an unrecognized keyword back until then, which is why `f(data=…)` is a -/// missing positional rather than an unexpected keyword. -fn arg_data_posonly( - args: &[PyObjectRef], - fn_name: &str, - kwonly: &[&str], -) -> Result<(PyObjectRef, Option), crate::PyError> { - let (pos, kwargs) = crate::builtins::split_builtin_kwargs(args); - crate::builtins::clinic_arity( - fn_name, - pos.len(), - crate::builtins::real_kwarg_count(kwargs), - 1, - 1, - kwonly.len(), - )?; - if pos.is_empty() { - return Err(crate::PyError::type_error(format!( - "{fn_name}() takes exactly 1 positional argument (0 given)" - ))); - } - if let Some(dict) = kwargs { - for (key, _) in unsafe { pyre_object::w_dict_str_entries_wtf8(dict) }.iter() { - // A keyword can be any `str`, so the name is compared and reported - // as the WTF-8 it is: `format!` would fold a surrogate to U+FFFD. - let named = key.as_str().ok(); - if named != Some("__pyre_kw__") && !named.is_some_and(|n| kwonly.contains(&n)) { - let mut msg = rustpython_wtf8::Wtf8Buf::from_string(format!( - "{fn_name}() got an unexpected keyword argument '" - )); - msg.push_wtf8(key); - msg.push_str("'"); - return Err(crate::PyError::type_error(msg)); - } - } +/// A slot the `Signature` binder resolved: `PY_NULL` for an omitted optional +/// argument, and `None` for one passed explicitly, both keep `default`. +fn slot_bool(w: PyObjectRef, default: bool) -> Result { + if w.is_null() || unsafe { is_none(w) } { + return Ok(default); } - Ok((pos[0], kwargs)) + crate::baseobjspace::is_true(w) } -/// A keyword-only flag: absent or `None` keeps `default`. -fn kwonly_bool(kwargs: Option, name: &str, default: bool) -> bool { - slot_bool( - crate::builtins::kwarg_get(kwargs, name).unwrap_or(PY_NULL), - default, - ) -} - -/// A slot [`crate::builtins::bind_builtin_kwargs`] resolved: `PY_NULL` for an -/// omitted optional argument, and `None` for one passed explicitly, both keep -/// `default`. -fn slot_bool(w: PyObjectRef, default: bool) -> bool { - if w.is_null() || unsafe { is_none(w) } { - return default; +/// An optional `oldcrc` slot: an omitted slot (`PY_NULL`) keeps `default`; +/// any supplied value — including `None` — goes through `truncatedint_w` +/// (`interp_crc32.py` `oldcrc='truncatedint_w'`), which raises `TypeError` +/// for `None` and keeps the low 32 bits of a wider integer. +fn slot_u32(w: PyObjectRef, default: u32) -> Result { + if w.is_null() { + return Ok(default); } - crate::baseobjspace::is_true(w).unwrap_or(default) + Ok(crate::baseobjspace::truncatedint_w(w)? as u32) } /// `ascii_buffer_converter` — accept a str (ASCII) or any bytes-like and @@ -234,23 +176,94 @@ crate::py_module! { "Error" => crate::builtins::lookup_exc_class("ValueError").expect("ValueError installed"), "Incomplete" => crate::builtins::lookup_exc_class("Exception").expect("Exception installed"), }, + inline_functions: { + // interp_hexlify.py:18 `hexlify(data, w_sep=None, w_bytes_per_sep=None)` + // — three positional-or-keyword slots. `b2a_hex` is the alias. + fn b2a_hex( + data: PyObjectRef, + #[default(w_none())] sep: PyObjectRef, + #[default(w_none())] bytes_per_sep: PyObjectRef, + ) -> Result { + hexlify_impl(data, sep, bytes_per_sep) + } + fn hexlify( + data: PyObjectRef, + #[default(w_none())] sep: PyObjectRef, + #[default(w_none())] bytes_per_sep: PyObjectRef, + ) -> Result { + hexlify_impl(data, sep, bytes_per_sep) + } + // interp_qp.py:19 `a2b_qp(data, header=0)` and interp_qp.py:97 + // `b2a_qp(data, quotetabs=0, istext=1, header=0)` — every slot + // positional-or-keyword. + fn a2b_qp( + data: PyObjectRef, + #[default(w_none())] header: PyObjectRef, + ) -> Result { + let data = as_bytes(arg_required(data, "a2b_qp", "data", 1)?)?; + let header = slot_bool(header, false)?; + Ok(w_bytes_from_bytes(&transforms::a2b_qp(&data, header))) + } + fn b2a_qp( + data: PyObjectRef, + #[default(w_none())] quotetabs: PyObjectRef, + #[default(w_none())] istext: PyObjectRef, + #[default(w_none())] header: PyObjectRef, + ) -> Result { + let data = as_buffer_bytes(arg_required(data, "b2a_qp", "data", 1)?)?; + let quotetabs = slot_bool(quotetabs, false)?; + let istext = slot_bool(istext, true)?; + let header = slot_bool(header, false)?; + Ok(w_bytes_from_bytes(&transforms::b2a_qp(&data, quotetabs, istext, header))) + } + // `a2b_base64(data, /, *, strict_mode=False)` — `data` positional-only, + // `strict_mode` keyword-only. interp_base64.py:39 + // `a2b_base64(ascii, strict_mode=0)` marks no `__kwonly__`, so upstream + // keeps `strict_mode` positional-or-keyword; the keyword-only form here + // predates this change and matches the 3.11+ accelerator signature. + fn a2b_base64( + data: PyObjectRef, + #[posonly] + #[kwonly] + #[default(w_none())] + strict_mode: PyObjectRef, + ) -> Result { + let data = as_bytes(arg_required(data, "a2b_base64", "data", 1)?)?; + let strict_mode = slot_bool(strict_mode, false)?; + let out = transforms::a2b_base64(&data, strict_mode).map_err(transform_error)?; + Ok(w_bytes_from_bytes(&out)) + } + // interp_base64.py:102 `b2a_base64(bin, __kwonly__, newline=True)` — + // `data` positional-only, `newline` keyword-only (the `__kwonly__` + // marker). + fn b2a_base64( + data: PyObjectRef, + #[posonly] + #[kwonly] + #[default(w_none())] + newline: PyObjectRef, + ) -> Result { + let data = as_buffer_bytes(arg_required(data, "b2a_base64", "data", 1)?)?; + let newline = slot_bool(newline, true)?; + Ok(w_bytes_from_bytes(&transforms::b2a_base64(&data, newline))) + } + // interp_uu.py:77 `b2a_uu(bin, __kwonly__, backtick=False)`. + fn b2a_uu( + data: PyObjectRef, + #[posonly] + #[kwonly] + #[default(w_none())] + backtick: PyObjectRef, + ) -> Result { + let data = as_buffer_bytes(arg_required(data, "b2a_uu", "data", 1)?)?; + let backtick = slot_bool(backtick, false)?; + let out = transforms::b2a_uu(&data, backtick).map_err(transform_error)?; + Ok(w_bytes_from_bytes(&out)) + } + }, functions: { - // `(data, sep=, bytes_per_sep=1)` — three - // positional-or-keyword slots. - "b2a_hex" / * = |args| { - let scope = crate::builtins::bind_builtin_kwargs( - args, &["data", "sep", "bytes_per_sep"], &[true, false, false], "b2a_hex")?; - let data = as_buffer_bytes(scope[0])?; - let (sep, bytes_per_sep) = sep_args(scope[1], scope[2])?; - Ok(w_bytes_from_bytes(&transforms::hexlify(&data, sep, bytes_per_sep))) - }, - "hexlify" / * = |args| { - let scope = crate::builtins::bind_builtin_kwargs( - args, &["data", "sep", "bytes_per_sep"], &[true, false, false], "hexlify")?; - let data = as_buffer_bytes(scope[0])?; - let (sep, bytes_per_sep) = sep_args(scope[1], scope[2])?; - Ok(w_bytes_from_bytes(&transforms::hexlify(&data, sep, bytes_per_sep))) - }, + // interp_hexlify.py:53 `unhexlify(hexstr)` / interp_uu.py:25 + // `a2b_uu(ascii)` — a single positional argument, no keyword surface. "a2b_hex" / 1 = |args| { let data = as_bytes(args.first().copied().unwrap_or(w_none()))?; let out = transforms::unhexlify(&data).map_err(transform_error)?; @@ -261,70 +274,109 @@ crate::py_module! { let out = transforms::unhexlify(&data).map_err(transform_error)?; Ok(w_bytes_from_bytes(&out)) }, - // `(data, crc=0, /)` and `(data, crc, /)` — positional-only throughout. - "crc32" / * = |args| { - let pos = unpack_positional(args, "crc32", 1, 2)?; - let data = as_buffer_bytes(pos[0])?; - let init = match pos.get(1) { - Some(&o) => crate::baseobjspace::int_w(o)? as u32, - None => 0, - }; - Ok(w_int_new(transforms::crc32(&data, init) as i64)) - }, - "crc_hqx" / * = |args| { - let pos = unpack_positional(args, "crc_hqx", 2, 2)?; - let data = as_buffer_bytes(pos[0])?; - let init = crate::baseobjspace::int_w(pos[1])? as u32; - Ok(w_int_new(transforms::crc_hqx(&data, init) as i64)) - }, - // `(data, /, *, flag=…)` — one positional-only slot, the rest - // keyword-only. - "a2b_base64" / * = |args| { - let (w_data, kwargs) = arg_data_posonly(args, "a2b_base64", &["strict_mode"])?; - let data = as_bytes(w_data)?; - let strict_mode = kwonly_bool(kwargs, "strict_mode", false); - let out = transforms::a2b_base64(&data, strict_mode).map_err(transform_error)?; - Ok(w_bytes_from_bytes(&out)) - }, - "b2a_base64" / * = |args| { - let (w_data, kwargs) = arg_data_posonly(args, "b2a_base64", &["newline"])?; - let data = as_buffer_bytes(w_data)?; - let newline = kwonly_bool(kwargs, "newline", true); - Ok(w_bytes_from_bytes(&transforms::b2a_base64(&data, newline))) - }, - // `(data, header=False)` / `(data, quotetabs, istext, header)` — every - // slot positional-or-keyword. - "a2b_qp" / * = |args| { - let scope = crate::builtins::bind_builtin_kwargs( - args, &["data", "header"], &[true, false], "a2b_qp")?; - let data = as_bytes(scope[0])?; - let header = slot_bool(scope[1], false); - Ok(w_bytes_from_bytes(&transforms::a2b_qp(&data, header))) - }, - "b2a_qp" / * = |args| { - let scope = crate::builtins::bind_builtin_kwargs( - args, - &["data", "quotetabs", "istext", "header"], - &[true, false, false, false], - "b2a_qp", - )?; - let data = as_buffer_bytes(scope[0])?; - let quotetabs = slot_bool(scope[1], false); - let istext = slot_bool(scope[2], true); - let header = slot_bool(scope[3], false); - Ok(w_bytes_from_bytes(&transforms::b2a_qp(&data, quotetabs, istext, header))) - }, "a2b_uu" / 1 = |args| { let data = as_bytes(args.first().copied().unwrap_or(w_none()))?; let out = transforms::a2b_uu(&data).map_err(transform_error)?; Ok(w_bytes_from_bytes(&out)) }, - "b2a_uu" / * = |args| { - let (w_data, kwargs) = arg_data_posonly(args, "b2a_uu", &["backtick"])?; - let data = as_buffer_bytes(w_data)?; - let backtick = kwonly_bool(kwargs, "backtick", false); - let out = transforms::b2a_uu(&data, backtick).map_err(transform_error)?; - Ok(w_bytes_from_bytes(&out)) - }, }, + extra_init: |ns| { + // interp_crc32.py:6 `crc32(data, oldcrc=0)` and interp_hqx.py:254 + // `crc_hqx(data, w_oldcrc)`. Both are all-positional-only, a shape the + // `#[pyre_function]` `#[posonly]` marker cannot express (there is no + // trailing parameter to mark the boundary before), so their + // `Signature` is built by hand: `posonlyargcount == argnames.len()`. + // With no keyword-only slot and `posonly == n_pos_params`, a keyword + // call is rejected as "() takes no keyword arguments". + // `oldcrc` is optional, so the argument count is not fixed: the + // signature is `HOPELESS` and the positional path routes through the + // binder rather than the fixed-arity fast entry. + crate::runtime_ops::module_ns_store( + ns, + "crc32", + crate::gateway::with_module( + "binascii", + crate::make_module_builtin_function_with_arity_and_maybe_sig( + "crc32", + crc32, + crate::HOPELESS, + Some(sig_all_posonly("crc32", &["data", "crc"])), + ), + ), + ); + // `HOPELESS` as well: a fixed arity would take the positional fast + // entry, which skips the binder and would let a surplus positional + // reach the body unchecked (`finish_builtin_code_positional`). + crate::runtime_ops::module_ns_store( + ns, + "crc_hqx", + crate::gateway::with_module( + "binascii", + crate::make_module_builtin_function_with_arity_and_maybe_sig( + "crc_hqx", + crc_hqx, + crate::HOPELESS, + Some(sig_all_posonly("crc_hqx", &["data", "crc"])), + ), + ), + ); + }, +} + +/// interp_hexlify.py:18 — the shared `hexlify` / `b2a_hex` body. `data` is a +/// bytes-like buffer; `sep` / `bytes_per_sep` are the length-1-ASCII separator +/// controls validated by [`sep_args`]. +fn hexlify_impl( + data: PyObjectRef, + sep: PyObjectRef, + bytes_per_sep: PyObjectRef, +) -> Result { + let data = as_buffer_bytes(arg_required(data, "hexlify", "data", 1)?)?; + let (sep, bytes_per_sep) = sep_args(sep, bytes_per_sep)?; + Ok(w_bytes_from_bytes(&transforms::hexlify( + &data, + sep, + bytes_per_sep, + ))) +} + +/// interp_crc32.py:6 `crc32(space, data, oldcrc=0)` — all-positional-only: +/// `data` required, `oldcrc` an optional truncated-int. +fn crc32(args: &[PyObjectRef]) -> Result { + let data = as_buffer_bytes(arg_required( + args.first().copied().unwrap_or(PY_NULL), + "crc32", + "data", + 1, + )?)?; + let init = slot_u32(args.get(1).copied().unwrap_or(PY_NULL), 0)?; + Ok(w_int_new(transforms::crc32(&data, init) as i64)) +} + +/// interp_hqx.py:254 `crc_hqx(space, data, w_oldcrc)` — both positional-only +/// and required. +fn crc_hqx(args: &[PyObjectRef]) -> Result { + let data = as_buffer_bytes(arg_required( + args.first().copied().unwrap_or(PY_NULL), + "crc_hqx", + "data", + 1, + )?)?; + let crc = arg_required(args.get(1).copied().unwrap_or(PY_NULL), "crc_hqx", "crc", 2)?; + let init = crate::baseobjspace::truncatedint_w(crc)? as u32; + Ok(w_int_new(transforms::crc_hqx(&data, init) as i64)) +} + +/// gateway `Signature` for an all-positional-only builtin: N appends then +/// `marker_posonly()` gives `posonlyargcount == N`. +fn sig_all_posonly(name: &'static str, names: &[&'static str]) -> crate::gateway::Signature { + let mut b = crate::SignatureBuilder { + name, + ..Default::default() + }; + for n in names { + b.append(n); + } + b.marker_posonly(); + b.signature() } diff --git a/pyre/pyre-interpreter/src/module/marshal/mod.rs b/pyre/pyre-interpreter/src/module/marshal/mod.rs index a7b13af4249..a47c476d0eb 100644 --- a/pyre/pyre-interpreter/src/module/marshal/mod.rs +++ b/pyre/pyre-interpreter/src/module/marshal/mod.rs @@ -401,22 +401,57 @@ impl wire::MarshalBag for PyreMarshalBag { } } -fn parse_version(positional: &[PyObjectRef], kwargs: Option) -> Result { - let value = - crate::builtins::kwarg_get(kwargs, "version").or_else(|| positional.get(1).copied()); - match value { +/// Resolve the optional `version` slot: an omitted slot uses the current +/// format version; an explicit value goes through `int_w`, so a `None` +/// raises `TypeError` like any other non-integer. +fn resolve_version(version: Option) -> Result { + match version { Some(value) => Ok(crate::baseobjspace::int_w(value)? as i32), None => Ok(wire::FORMAT_VERSION as i32), } } -fn parse_allow_code(kwargs: Option) -> Result { - match crate::builtins::kwarg_get(kwargs, "allow_code") { +/// Resolve the keyword-only `allow_code` slot by truth-testing; an omitted +/// slot defaults to true. +fn resolve_allow_code(allow_code: Option) -> Result { + match allow_code { Some(value) => crate::baseobjspace::is_true(value), None => Ok(true), } } +/// Serialize one object to a marshal byte stream at `version`. With +/// `allow_code` false, a nested code object is rejected before writing +/// (marshal.check_no_code). +fn marshal_to_bytes( + value: PyObjectRef, + version: i32, + allow_code: bool, +) -> Result, PyError> { + if !allow_code { + reject_code(value)?; + } + let _roots = pyre_object::gc_roots::push_roots(); + pyre_object::gc_roots::pin_root(value); + let mut out = Vec::new(); + let mut refs = (version >= 3).then(WriterRefs::new); + write_object(&mut out, value, &mut refs, version, MAX_DEPTH)?; + Ok(out) +} + +/// Deserialize one object from a marshal byte stream. With `allow_code` +/// false, a decoded code object is rejected (marshal.check_no_code). +fn unmarshal_bytes(data: &[u8], allow_code: bool) -> PyResult { + let _roots = pyre_object::gc_roots::push_roots(); + let mut reader: &[u8] = data; + let result = wire::deserialize_value(&mut reader, PyreMarshalBag).map_err(marshal_error)?; + let result = result.get(); + if !allow_code { + reject_code(result)?; + } + Ok(result) +} + /// RustPython `marshal.check_no_code`, matching CPython's recursive /// `allow_code=False` check. The Vec is a transient identity set (PyPy's /// traversal does not persist it); it also prevents container cycles from @@ -466,61 +501,11 @@ fn reject_code(value: PyObjectRef) -> Result<(), PyError> { } } -fn dumps_impl(args: &[PyObjectRef]) -> PyResult { - let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args); - crate::builtins::kwarg_reject_unknown(kwargs, &["version", "allow_code"], "dumps")?; - let Some(&value) = positional.first() else { - return Err(PyError::type_error( - "dumps() missing required argument 'value'", - )); - }; - if positional.len() > 2 { - return Err(PyError::type_error("dumps() takes at most 2 arguments")); - } - let version = parse_version(positional, kwargs)?; - let allow_code = parse_allow_code(kwargs)?; - if !allow_code { - reject_code(value)?; - } - let _roots = pyre_object::gc_roots::push_roots(); - pyre_object::gc_roots::pin_root(value); - let mut out = Vec::new(); - let mut refs = (version >= 3).then(WriterRefs::new); - write_object(&mut out, value, &mut refs, version, MAX_DEPTH)?; - Ok(bytesobject::w_bytes_from_bytes(&out)) -} - -fn loads_impl(args: &[PyObjectRef]) -> PyResult { - let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args); - crate::builtins::kwarg_reject_unknown(kwargs, &["allow_code"], "loads")?; - let Some(&data) = positional.first() else { - return Err(PyError::type_error( - "loads() missing required argument 'bytes'", - )); - }; - if positional.len() != 1 { - return Err(PyError::type_error("loads() takes exactly 1 argument")); - } - let allow_code = parse_allow_code(kwargs)?; - let data = bytes_like(data, "loads")?; - let _roots = pyre_object::gc_roots::push_roots(); - let mut reader: &[u8] = &data; - let result = wire::deserialize_value(&mut reader, PyreMarshalBag).map_err(marshal_error)?; - let result = result.get(); - if !allow_code { - reject_code(result)?; - } - Ok(result) -} - /// `PyMarshal_ReadObjectFromString` — deserialize one object out of a raw byte /// buffer. `_imp.get_frozen_object` unmarshals caller-supplied frozen data /// through this and rewrites any failure into its own diagnostic. pub(crate) fn loads_bytes(data: &[u8]) -> PyResult { - let _roots = pyre_object::gc_roots::push_roots(); - let mut reader: &[u8] = data; - let result = wire::deserialize_value(&mut reader, PyreMarshalBag).map_err(marshal_error)?; - Ok(result.get()) + unmarshal_bytes(data, true) } /// `PyMarshal_WriteObjectToString` — serialize one object into a raw byte @@ -528,57 +513,7 @@ pub(crate) fn loads_bytes(data: &[u8]) -> PyResult { /// hands these bytes back as the frozen data, so they are the same stream /// `loads_bytes` reads. pub(crate) fn dumps_bytes(value: PyObjectRef) -> Result, PyError> { - let version = wire::FORMAT_VERSION as i32; - let _roots = pyre_object::gc_roots::push_roots(); - pyre_object::gc_roots::pin_root(value); - let mut out = Vec::new(); - let mut refs = (version >= 3).then(WriterRefs::new); - write_object(&mut out, value, &mut refs, version, MAX_DEPTH)?; - Ok(out) -} - -fn dump_impl(args: &[PyObjectRef]) -> PyResult { - let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args); - crate::builtins::kwarg_reject_unknown(kwargs, &["version", "allow_code"], "dump")?; - if positional.len() < 2 || positional.len() > 3 { - return Err(PyError::type_error("dump() expected 2 or 3 arguments")); - } - let mut dump_args = vec![positional[0]]; - if let Some(version) = positional.get(2) { - dump_args.push(*version); - } - if let Some(kwargs) = kwargs { - dump_args.push(kwargs); - } - let bytes = dumps_impl(&dump_args)?; - call_method(positional[1], "write", &[bytes])?; - Ok(w_none()) -} - -fn load_impl(args: &[PyObjectRef]) -> PyResult { - let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args); - crate::builtins::kwarg_reject_unknown(kwargs, &["allow_code"], "load")?; - if positional.len() != 1 { - return Err(PyError::type_error("load() takes exactly 1 argument")); - } - let file = positional[0]; - let before = crate::baseobjspace::int_w(call_method(file, "tell", &[])?)?; - let bytes_obj = call_method(file, "read", &[])?; - let data = bytes_like(bytes_obj, "load")?; - let allow_code = parse_allow_code(kwargs)?; - let _roots = pyre_object::gc_roots::push_roots(); - let mut reader = wire::Cursor { - data: data.as_slice(), - position: 0, - }; - let result = wire::deserialize_value(&mut reader, PyreMarshalBag).map_err(marshal_error)?; - let new_position = w_int_new(before.saturating_add(reader.position as i64)); - call_method(file, "seek", &[new_position])?; - let result = result.get(); - if !allow_code { - reject_code(result)?; - } - Ok(result) + marshal_to_bytes(value, wire::FORMAT_VERSION as i32, true) } crate::py_module! { @@ -586,10 +521,81 @@ crate::py_module! { int_constants: { "version" => wire::FORMAT_VERSION as i64, }, - functions: { - "dump" / * = dump_impl, - "dumps" / * = dumps_impl, - "load" / * = load_impl, - "loads" / * = loads_impl, + inline_functions: { + // interp_marshal.py:33 `dumps(w_data, version=Py_MARSHAL_VERSION)` — + // `value` / `version` positional-only, `allow_code` keyword-only + // (the 3.13 accelerator signature `dumps(value, version, /, *, + // allow_code=True)`). `version` stays `Option` so an explicit + // `None` reaches `int_w` and raises rather than defaulting. + fn dumps( + value: PyObjectRef, + version: Option, + #[posonly] + #[kwonly] + allow_code: Option, + ) -> Result { + let version = resolve_version(version)?; + let allow_code = resolve_allow_code(allow_code)?; + let out = marshal_to_bytes(value, version, allow_code)?; + Ok(bytesobject::w_bytes_from_bytes(&out)) + } + // interp_marshal.py:49 `loads(w_str)` — `bytes` positional-only, + // `allow_code` keyword-only (`loads(bytes, /, *, allow_code=True)`). + fn loads( + data: PyObjectRef, + #[posonly] + #[kwonly] + allow_code: Option, + ) -> Result { + let allow_code = resolve_allow_code(allow_code)?; + let data = bytes_like(data, "loads")?; + unmarshal_bytes(&data, allow_code) + } + // interp_marshal.py:26 `dump(w_data, w_f, version=Py_MARSHAL_VERSION)` + // — writes the stream `dumps` would return to `f.write` + // (`dump(value, file, version, /, *, allow_code=True)`). + fn dump( + value: PyObjectRef, + file: PyObjectRef, + version: Option, + #[posonly] + #[kwonly] + allow_code: Option, + ) -> Result { + let version = resolve_version(version)?; + let allow_code = resolve_allow_code(allow_code)?; + let out = marshal_to_bytes(value, version, allow_code)?; + let bytes = bytesobject::w_bytes_from_bytes(&out); + call_method(file, "write", &[bytes])?; + Ok(w_none()) + } + // interp_marshal.py:40 `load(w_f)` reads one value from `f` and + // rewinds `f` past exactly the bytes consumed + // (`load(file, /, *, allow_code=True)`). + fn load( + file: PyObjectRef, + #[posonly] + #[kwonly] + allow_code: Option, + ) -> Result { + let allow_code = resolve_allow_code(allow_code)?; + let before = crate::baseobjspace::int_w(call_method(file, "tell", &[])?)?; + let bytes_obj = call_method(file, "read", &[])?; + let data = bytes_like(bytes_obj, "load")?; + let _roots = pyre_object::gc_roots::push_roots(); + let mut reader = wire::Cursor { + data: data.as_slice(), + position: 0, + }; + let result = + wire::deserialize_value(&mut reader, PyreMarshalBag).map_err(marshal_error)?; + let new_position = w_int_new(before.saturating_add(reader.position as i64)); + call_method(file, "seek", &[new_position])?; + let result = result.get(); + if !allow_code { + reject_code(result)?; + } + Ok(result) + } }, } diff --git a/pyre/pyre-interpreter/src/module/struct/mod.rs b/pyre/pyre-interpreter/src/module/struct/mod.rs index fac3f398ed1..8ce6e357b71 100644 --- a/pyre/pyre-interpreter/src/module/struct/mod.rs +++ b/pyre/pyre-interpreter/src/module/struct/mod.rs @@ -1178,32 +1178,26 @@ impl W_Struct { do_unpack(fmt, buf) } - /// `interp_struct.py:231 descr_pack_into` — - /// `do_pack_into(space, jit.promote_string(self.format), buffer, offset, args_w)`. - /// Whole-args ABI: `args[0]` = self, `args[1]` = buffer, `args[2]` = - /// offset, `args[3..]` = the packed values. - fn pack_into(&self, args: &[PyObjectRef]) -> Result { - self.ensure_ready()?; - let format = majit_metainterp::jit::promote_string(self.format); - let fmt = unsafe { w_str_get_value(format) }; - let (pos, _) = crate::builtins::split_builtin_kwargs(&args[1..]); - if pos.len() < 2 { - return Err(crate::PyError::type_error( - "pack_into() missing buffer or offset argument", - )); - } - let offset = unsafe { crate::builtins::space_index_w(pos[1])? }; - do_pack_into(fmt, pos[0], offset, &pos[2..]) - } - - /// `interp_struct.py:238 descr_unpack_from` — + /// `interp_struct.py:275 descr_unpack_from(self, w_buffer, offset=0)` — /// `do_unpack_from(space, jit.promote_string(self.format), buffer, offset)`. - /// `buffer` / `offset` are accepted positionally or by keyword. - fn unpack_from(&self, args: &[PyObjectRef]) -> Result { + /// `self` positional-only; `buffer` / `offset` positional-or-keyword. + /// `offset` is `Option` so an omitted slot becomes `0` while an explicit + /// `offset=None` reaches `space_index_w` and raises. + fn unpack_from( + &self, + buffer: PyObjectRef, + offset: Option, + ) -> Result { self.ensure_ready()?; let format = majit_metainterp::jit::promote_string(self.format); let fmt = unsafe { w_str_get_value(format) }; - let (buffer, offset) = resolve_buffer_offset(&args[1..])?; + // Coerce `offset` before borrowing the buffer: `space_index_w` may run + // `__index__`, which can resize or free the backing store `readbuf` + // hands back as a raw slice. + let offset = match offset { + Some(o) => unsafe { crate::builtins::space_index_w(o)? }, + None => 0, + }; let buf = unsafe { readbuf(buffer)? }; do_unpack_from(fmt, buf, offset) } @@ -1227,26 +1221,80 @@ impl W_Struct { } } -/// Resolve `(buffer, offset=0)` from a positional/keyword argument slice -/// (`unpack_from` accepts both `buffer=` and `offset=`). -fn resolve_buffer_offset(args: &[PyObjectRef]) -> Result<(PyObjectRef, i64), crate::PyError> { - let (pos, kwargs) = crate::builtins::split_builtin_kwargs(args); - let buffer = pos - .first() - .copied() - .or_else(|| crate::builtins::kwarg_get(kwargs, "buffer")) - .ok_or_else(|| { - crate::PyError::type_error("unpack_from() missing required argument 'buffer'") - })?; - let offset = match pos - .get(1) - .copied() - .or_else(|| crate::builtins::kwarg_get(kwargs, "offset")) - { - Some(o) => unsafe { crate::builtins::space_index_w(o)? }, - None => 0, +/// gateway `Signature` for a builtin whose named parameters are all +/// positional-only followed by a `*args` tail: N appends then +/// `marker_posonly()` gives `posonlyargcount == N`, and `varargname` +/// records the tail. With no keyword-only slot and `posonly == +/// n_pos_params`, a stray keyword is rejected as "() takes no keyword +/// arguments" before the tail is packed (`bind_kwargs_to_signature`). +fn sig_posonly_then_varargs( + name: &'static str, + names: &[&'static str], + varargname: &'static str, +) -> crate::gateway::Signature { + let mut b = crate::SignatureBuilder { + name, + varargname: Some(varargname), + ..Default::default() }; - Ok((buffer, offset)) + for n in names { + b.append(n); + } + b.marker_posonly(); + b.signature() +} + +/// `interp_struct.py:66 pack(space, w_format, args_w)`. The binder hands a +/// bound slice `[format, (*args tuple)]`: the named `format` slot then the +/// packed vararg tail as a single tuple. A stray keyword is rejected by the +/// binder before the body runs (`posonlyargcount == 1`, no `**kwargs`). The +/// binder pads an absent positional with `PY_NULL` rather than raising, so the +/// required `format` slot is checked here. +fn pack(args: &[PyObjectRef]) -> Result { + if args[0].is_null() { + return Err(crate::PyError::type_error("missing format argument")); + } + let fmt = format_to_string(args[0])?; + let values = unsafe { w_tuple_items_copy_as_vec(args[1]) }; + do_pack(&fmt, &values) +} + +/// `interp_struct.py:76 pack_into(space, w_format, w_buffer, offset, args_w)`. +/// Bound slice `[format, buffer, offset, (*args tuple)]`. As with [`pack`], +/// the three required positional slots are `PY_NULL`-checked here. +fn pack_into(args: &[PyObjectRef]) -> Result { + if args[0].is_null() || args[1].is_null() || args[2].is_null() { + return Err(crate::PyError::type_error( + "pack_into() missing format, buffer or offset argument", + )); + } + let fmt = format_to_string(args[0])?; + let offset = unsafe { crate::builtins::space_index_w(args[2])? }; + let values = unsafe { w_tuple_items_copy_as_vec(args[3]) }; + do_pack_into(&fmt, args[1], offset, &values) +} + +/// `interp_struct.py:268 descr_pack_into(self, w_buffer, offset, args_w)` — +/// the `Struct` method form. Bound slice `[self, buffer, offset, +/// (*args tuple)]`. Registered by hand (below) because the +/// `#[pyre_methods]` arm cannot emit a `varargname`-bearing `Signature` for a +/// `&[PyObjectRef]` method. +fn struct_pack_into(args: &[PyObjectRef]) -> Result { + let this = W_Struct::from_obj(args[0]) + .ok_or_else(|| crate::PyError::type_error("descriptor 'pack_into' got wrong receiver"))?; + // The binder pads an omitted `buffer` / `offset` with PY_NULL; report the + // missing argument before `space_index_w` reads a null slot. + if args[1].is_null() || args[2].is_null() { + return Err(crate::PyError::type_error( + "pack_into() missing buffer or offset argument", + )); + } + this.ensure_ready()?; + let format = majit_metainterp::jit::promote_string(this.format); + let fmt = unsafe { w_str_get_value(format) }; + let offset = unsafe { crate::builtins::space_index_w(args[2])? }; + let values = unsafe { w_tuple_items_copy_as_vec(args[3]) }; + do_pack_into(fmt, args[1], offset, &values) } // ── W_UnpackIter ───────────────────────────────────────────────────── @@ -1469,51 +1517,30 @@ crate::py_module! { let buf = unsafe { readbuf(buffer)? }; do_unpack(&fmt, buf) } - }, - functions: { - // `pack(fmt, *args)` — variadic positional after fmt; route - // through the args slice (typed varargs are not supported by - // inline_functions arity inference). - "pack" / * = |args| { - let (args, kwargs) = crate::builtins::split_builtin_kwargs(args); - if crate::builtins::has_real_kwargs(kwargs) { - return Err(crate::PyError::type_error( - "_struct.pack() takes no keyword arguments", - )); - } - if args.is_empty() { - return Err(crate::PyError::type_error("missing format argument")); - } - let fmt = format_to_string(args[0])?; - do_pack(&fmt, &args[1..]) - }, - // `pack_into(fmt, buffer, offset, *args)` — write the packed bytes - // into a writable buffer at `offset`. - "pack_into" / * = |args| { - let (pos, _) = crate::builtins::split_builtin_kwargs(args); - if pos.len() < 3 { - return Err(crate::PyError::type_error( - "pack_into() missing format, buffer or offset argument", - )); - } - let fmt = format_to_string(pos[0])?; - let offset = unsafe { crate::builtins::space_index_w(pos[2])? }; - do_pack_into(&fmt, pos[1], offset, &pos[3..]) - }, - // `unpack_from(fmt, /, buffer, offset=0)` — `buffer` / `offset` - // are accepted positionally or by keyword. - "unpack_from" / * = |args| { - let (pos, _) = crate::builtins::split_builtin_kwargs(args); - if pos.is_empty() { - return Err(crate::PyError::type_error( - "unpack_from() missing required argument 'format'", - )); - } - let fmt = format_to_string(pos[0])?; - let (buffer, offset) = resolve_buffer_offset(&args[1..])?; + // `interp_struct.py:154 unpack_from(space, w_format, w_buffer, offset=0)`. + // `format` positional-only; `buffer` / `offset` positional-or-keyword. + // `offset` is `Option` rather than `#[default]` so an omitted slot + // (`None`) becomes `0` while an explicit `offset=None` reaches + // `space_index_w` and raises the "cannot be interpreted as an integer" + // TypeError. + fn unpack_from( + fmt_obj: PyObjectRef, + #[posonly] buffer: PyObjectRef, + offset: Option, + ) -> Result { + let fmt = format_to_string(fmt_obj)?; + // Coerce `offset` before borrowing the buffer: `space_index_w` may + // run `__index__`, which can resize or free the backing store + // `readbuf` hands back as a raw slice. + let offset = match offset { + Some(o) => unsafe { crate::builtins::space_index_w(o)? }, + None => 0, + }; let buf = unsafe { readbuf(buffer)? }; do_unpack_from(&fmt, buf, offset) - }, + } + }, + functions: { // `iter_unpack(fmt, buffer)` — an iterator over the records. "iter_unpack" / 2 = |args| { let fmt = format_to_string(args[0])?; @@ -1532,5 +1559,68 @@ crate::py_module! { base, ); crate::module_ns_store(ns, "error", error); + // `interp_struct.py:66 pack(w_format, args_w)` and + // `interp_struct.py:76 pack_into(w_format, w_buffer, offset, args_w)` + // — `*args`-carrying builtins whose named parameters are all + // positional-only. The `inline_functions:` / `functions:` arms cannot + // emit a `varargname`-bearing `Signature` (a `&[PyObjectRef]` slice + // suppresses the signature), so both are registered by hand with a + // vararg `Signature`. `has_vararg()` forces `HOPELESS`, routing the + // positional path through the binder, which packs the excess + // positionals into the tuple the body reads. + crate::module_ns_store( + ns, + "pack", + crate::gateway::with_module( + "_struct", + crate::make_module_builtin_function_with_arity_and_maybe_sig( + "pack", + pack, + crate::HOPELESS, + Some(sig_posonly_then_varargs("pack", &["format"], "args")), + ), + ), + ); + crate::module_ns_store( + ns, + "pack_into", + crate::gateway::with_module( + "_struct", + crate::make_module_builtin_function_with_arity_and_maybe_sig( + "pack_into", + pack_into, + crate::HOPELESS, + Some(sig_posonly_then_varargs( + "pack_into", + &["format", "buffer", "offset"], + "args", + )), + ), + ), + ); + // `interp_struct.py:268 descr_pack_into(self, w_buffer, offset, args_w)` + // — the `*args` method form. The `#[pyre_methods]` arm gives a + // `&[PyObjectRef]` method a null `Signature` (raw whole-args + // passthrough), so it is registered by hand into the `Struct` type + // dict with a vararg `Signature` (`self` + two positional-only slots + + // `*args`). `type_object()` is idempotent (`OnceLock`); the + // `interpleveldefs` entry has already built the type. + let struct_type = type_object(); + let struct_dict = unsafe { pyre_object::w_type_get_dict_ptr(struct_type) } as PyObjectRef; + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + struct_dict, + "pack_into", + crate::make_builtin_function_maybe_sig( + "pack_into", + struct_pack_into, + Some(sig_posonly_then_varargs( + "pack_into", + &["self", "buffer", "offset"], + "args", + )), + ), + ); + } }, } diff --git a/pyre/pyre-interpreter/src/module/zlib/mod.rs b/pyre/pyre-interpreter/src/module/zlib/mod.rs index a82b7ea20bf..aea4b17faff 100644 --- a/pyre/pyre-interpreter/src/module/zlib/mod.rs +++ b/pyre/pyre-interpreter/src/module/zlib/mod.rs @@ -81,30 +81,25 @@ fn as_bytes(obj: PyObjectRef) -> Result, crate::PyError> { } } -/// Fetch an integer argument by keyword or position, `None`/missing → default. -fn arg_int( - pos: &[PyObjectRef], - kwargs: Option, - name: &str, - index: usize, - default: i64, -) -> Result { - match crate::builtins::kwarg_get(kwargs, name).or_else(|| pos.get(index).copied()) { - Some(o) if unsafe { is_none(o) } => Ok(default), - Some(o) => crate::baseobjspace::int_w(o), - None => Ok(default), +/// Coerce an argument slot to an integer, `None`/absent (`PY_NULL`) → default. +/// +/// The Signature-bound call path fills every declared slot: a supplied +/// keyword / positional carries the value, an omitted optional carries +/// `PY_NULL`. +fn int_or_default(o: PyObjectRef, default: i64) -> Result { + if o.is_null() || unsafe { is_none(o) } { + Ok(default) + } else { + crate::baseobjspace::int_w(o) } } -/// Fetch an optional `zdict` bytes argument by keyword or position. -fn arg_zdict( - pos: &[PyObjectRef], - kwargs: Option, - index: usize, -) -> Result>, crate::PyError> { - match crate::builtins::kwarg_get(kwargs, "zdict").or_else(|| pos.get(index).copied()) { - Some(o) if !unsafe { is_none(o) } => Ok(Some(as_bytes(o)?)), - _ => Ok(None), +/// Coerce an optional `zdict` slot to bytes, `None`/absent (`PY_NULL`) → None. +fn zdict_or_none(o: PyObjectRef) -> Result>, crate::PyError> { + if o.is_null() || unsafe { is_none(o) } { + Ok(None) + } else { + Ok(Some(as_bytes(o)?)) } } @@ -248,8 +243,12 @@ fn init_decompress_type(ns: PyObjectRef) { } let id = get_id(args[0]); let data = as_bytes(args[1])?; + // An omitted `max_length` is unlimited; a supplied value — + // including `None` — goes through `int_w`, which raises for + // `None`. Zero also means unlimited here, and a negative + // value is rejected. let max_length = match args.get(2).copied() { - Some(o) if !unsafe { is_none(o) } => { + Some(o) => { let v = crate::baseobjspace::int_w(o)?; if v < 0 { return Err(crate::PyError::value_error( @@ -258,7 +257,7 @@ fn init_decompress_type(ns: PyObjectRef) { } (v != 0).then_some(v as usize) } - _ => None, + None => None, }; let mut reg = DECOMPRESSORS.lock().unwrap(); let d = reg @@ -356,59 +355,90 @@ fn zdecompress_getset(ns: PyObjectRef, name: &'static str, f: crate::gateway::Bu }; } +// _ZlibDecompressor(wbits=MAX_WBITS, zdict=b'') — the DecompressReader factory +// gzip calls with wbits=-MAX_WBITS. `cls` positional-only, `wbits`/`zdict` +// positional-or-keyword; the Signature-bound call path fills omitted optionals +// with PY_NULL. +fn zdecompress_new(args: &[PyObjectRef]) -> Result { + // args[0] is the type; args[1..] are the constructor arguments. + let wbits = to_wbits(int_or_default( + args.get(1).copied().unwrap_or(PY_NULL), + backend::MAX_WBITS as i64, + )?); + let zdict = zdict_or_none(args.get(2).copied().unwrap_or(PY_NULL))?; + let d = backend::ZlibDecompressor::new(wbits, zdict).map_err(zlib_error)?; + let id = next_id(); + ZDECOMPRESSORS.lock().unwrap().insert(id, d); + let obj = w_instance_new(zdecompress_type()); + set_id(obj, id); + Ok(obj) +} + +// `_ZlibDecompressor.decompress(self, /, data, max_length=-1)` — `self` +// positional-only, `data` positional-or-keyword. +fn zdecompress_decompress(args: &[PyObjectRef]) -> Result { + let data_obj = args.get(1).copied().unwrap_or(PY_NULL); + if data_obj.is_null() { + return Err(crate::PyError::type_error("decompress() missing data")); + } + let data = as_bytes(data_obj)?; + // `max_length=-1` (the default) means unlimited; an omitted slot behaves + // the same. Only PY_NULL selects that default: a supplied value — + // including `None` — goes through `int_w`, which raises for `None` and on + // ssize_t overflow. A negative value is unlimited, zero caps the output + // at zero bytes. + let max_length = match args.get(2).copied() { + Some(o) if !o.is_null() => { + let v = crate::baseobjspace::int_w(o)?; + (v >= 0).then_some(v as usize) + } + _ => None, + }; + let id = get_id(args[0]); + let mut reg = ZDECOMPRESSORS.lock().unwrap(); + let d = reg + .get_mut(&id) + .ok_or_else(|| zlib_error("Error -2: inconsistent stream state"))?; + match d.decompress(&data, max_length) { + Ok(out) => Ok(bytesobject::w_bytes_from_bytes(&out)), + Err(backend::DecompressError::Zlib(m)) => Err(zlib_error(m)), + Err(backend::DecompressError::Eof) => Err(eof_error("End of stream already reached")), + } +} + fn init_zdecompress_type(ns: PyObjectRef) { - // _ZlibDecompressor(wbits=MAX_WBITS, zdict=b'') — the DecompressReader - // factory gzip calls with wbits=-MAX_WBITS. + let new_sig = { + let mut b = crate::SignatureBuilder::default(); + b.append("cls"); + b.marker_posonly(); + b.append("wbits"); + b.append("zdict"); + b.signature() + }; unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, "__new__", - crate::typedef::make_new_descr(|args| { - // args[0] is the type; the rest are the constructor arguments. - let (pos, kwargs) = crate::builtins::split_builtin_kwargs(&args[1..]); - let wbits = to_wbits(arg_int(pos, kwargs, "wbits", 0, backend::MAX_WBITS as i64)?); - let zdict = arg_zdict(pos, kwargs, 1)?; - let d = backend::ZlibDecompressor::new(wbits, zdict).map_err(zlib_error)?; - let id = next_id(); - ZDECOMPRESSORS.lock().unwrap().insert(id, d); - let obj = w_instance_new(zdecompress_type()); - set_id(obj, id); - Ok(obj) - }), + crate::typedef::make_new_descr_maybe_sig(zdecompress_new, Some(new_sig)), ) }; + let decompress_sig = { + let mut b = crate::SignatureBuilder::default(); + b.append("self"); + b.marker_posonly(); + b.append("data"); + b.append("max_length"); + b.signature() + }; unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, "decompress", - crate::make_builtin_function("decompress", |args| { - if args.len() < 2 { - return Err(crate::PyError::type_error("decompress() missing data")); - } - let (pos, kwargs) = crate::builtins::split_builtin_kwargs(&args[1..]); - let data = as_bytes(pos.first().copied().unwrap_or(w_none()))?; - let max_length = match crate::builtins::kwarg_get(kwargs, "max_length") - .or_else(|| pos.get(1).copied()) - { - Some(o) if !unsafe { is_none(o) } => { - let v = crate::baseobjspace::int_w(o)?; - usize::try_from(v).ok() - } - _ => None, - }; - let id = get_id(args[0]); - let mut reg = ZDECOMPRESSORS.lock().unwrap(); - let d = reg - .get_mut(&id) - .ok_or_else(|| zlib_error("Error -2: inconsistent stream state"))?; - match d.decompress(&data, max_length) { - Ok(out) => Ok(bytesobject::w_bytes_from_bytes(&out)), - Err(backend::DecompressError::Zlib(m)) => Err(zlib_error(m)), - Err(backend::DecompressError::Eof) => { - Err(eof_error("End of stream already reached")) - } - } - }), + crate::make_builtin_function_maybe_sig( + "decompress", + zdecompress_decompress, + Some(decompress_sig), + ), ) }; zdecompress_getset(ns, "unused_data", |args| { @@ -466,6 +496,66 @@ crate::py_module! { exceptions: { "error" => crate::builtins::lookup_exc_class("Exception").expect("Exception installed"), }, + inline_functions: { + // interp_zlib.py:66 `compress(data, __posonly__=None, level, wbits)` — + // `data` positional-only, `level`/`wbits` positional-or-keyword. + fn compress( + data: PyBufferStr, + #[posonly] + #[default(w_none())] + level: PyObjectRef, + #[default(w_none())] + wbits: PyObjectRef, + ) -> Result { + let level = int_or_default(level, -1)? as i32; + let wbits = to_wbits(int_or_default(wbits, backend::MAX_WBITS as i64)?); + let out = backend::compress(data, level, wbits).map_err(zlib_error)?; + Ok(bytesobject::w_bytes_from_bytes(&out)) + } + // interp_zlib.py:92 `decompress(string, __posonly__=None, wbits, bufsize)`. + fn decompress( + data: PyBufferStr, + #[posonly] + #[default(w_none())] + wbits: PyObjectRef, + #[default(w_none())] + bufsize: PyObjectRef, + ) -> Result { + let wbits = to_wbits(int_or_default(wbits, backend::MAX_WBITS as i64)?); + let bufsize = int_or_default(bufsize, backend::DEF_BUF_SIZE as i64)?; + if bufsize < 0 { + return Err(crate::PyError::value_error("bufsize must be non-negative")); + } + let out = backend::decompress(data, wbits, bufsize as usize).map_err(zlib_error)?; + Ok(bytesobject::w_bytes_from_bytes(&out)) + } + // interp_zlib.py:228 `Compress___new__(level, method, wbits, memLevel, + // strategy, w_zdict)` — all six positional-or-keyword. `method` / + // `memLevel` / `strategy` are accepted-and-ignored: `Compressor::new` + // threads only level / wbits / zdict (interp_zlib.py:244 notes the + // undocumented pass-through). + fn compressobj( + #[default(w_none())] level: PyObjectRef, + #[default(w_none())] method: PyObjectRef, + #[default(w_none())] wbits: PyObjectRef, + #[default(w_none())] memLevel: PyObjectRef, + #[default(w_none())] strategy: PyObjectRef, + #[default(w_none())] zdict: PyObjectRef, + ) -> Result { + let _ = (method, memLevel, strategy); + let level = int_or_default(level, -1)? as i32; + let wbits = to_wbits(int_or_default(wbits, backend::MAX_WBITS as i64)?); + make_compress(level, wbits, zdict_or_none(zdict)?) + } + // interp_zlib.py:400 `Decompress___new__(wbits, w_zdict)`. + fn decompressobj( + #[default(w_none())] wbits: PyObjectRef, + #[default(w_none())] zdict: PyObjectRef, + ) -> Result { + let wbits = to_wbits(int_or_default(wbits, backend::MAX_WBITS as i64)?); + make_decompress(wbits, zdict_or_none(zdict)?) + } + }, functions: { "crc32" / * = |args| { let data = as_bytes(args.first().copied().unwrap_or(w_none()))?; @@ -477,39 +567,5 @@ crate::py_module! { let start = args.get(1).map(|&o| unsafe { w_int_get_value(o) } as u32).unwrap_or(1); Ok(w_int_new(adler32_compute(&data, start) as i64)) }, - "compress" / * = |args| { - let (pos, kwargs) = crate::builtins::split_builtin_kwargs(args); - let data = as_bytes(pos.first().copied().unwrap_or(w_none()))?; - let level = arg_int(pos, kwargs, "level", 1, -1)? as i32; - let wbits = to_wbits(arg_int(pos, kwargs, "wbits", 2, backend::MAX_WBITS as i64)?); - let out = backend::compress(&data, level, wbits).map_err(zlib_error)?; - Ok(bytesobject::w_bytes_from_bytes(&out)) - }, - "decompress" / * = |args| { - let (pos, kwargs) = crate::builtins::split_builtin_kwargs(args); - let data = as_bytes(pos.first().copied().unwrap_or(w_none()))?; - let wbits = to_wbits(arg_int(pos, kwargs, "wbits", 1, backend::MAX_WBITS as i64)?); - let bufsize = arg_int(pos, kwargs, "bufsize", 2, backend::DEF_BUF_SIZE as i64)?; - if bufsize < 0 { - return Err(crate::PyError::value_error("bufsize must be non-negative")); - } - let out = backend::decompress(&data, wbits, bufsize as usize).map_err(zlib_error)?; - Ok(bytesobject::w_bytes_from_bytes(&out)) - }, - "compressobj" / * = |args| { - let (pos, kwargs) = crate::builtins::split_builtin_kwargs(args); - let level = arg_int(pos, kwargs, "level", 0, -1)? as i32; - // positions 1 (method) and 3 (memLevel) / 4 (strategy) are accepted - // but only level / wbits / zdict affect the stream. - let wbits = to_wbits(arg_int(pos, kwargs, "wbits", 2, backend::MAX_WBITS as i64)?); - let zdict = arg_zdict(pos, kwargs, 5)?; - make_compress(level, wbits, zdict) - }, - "decompressobj" / * = |args| { - let (pos, kwargs) = crate::builtins::split_builtin_kwargs(args); - let wbits = to_wbits(arg_int(pos, kwargs, "wbits", 0, backend::MAX_WBITS as i64)?); - let zdict = arg_zdict(pos, kwargs, 1)?; - make_decompress(wbits, zdict) - }, }, } diff --git a/pyre/pyre-jit/src/call_jit.rs b/pyre/pyre-jit/src/call_jit.rs index a48bfde0b3f..ccd716d0c31 100644 --- a/pyre/pyre-jit/src/call_jit.rs +++ b/pyre/pyre-jit/src/call_jit.rs @@ -5093,9 +5093,14 @@ fn bh_call_fn_impl(callable: PyObjectRef, null_or_self: PyObjectRef, args: &[PyO let code = unsafe { pyre_interpreter::getcode(callable) }; if unsafe { pyre_interpreter::is_builtin_code(code as pyre_object::PyObjectRef) } { let call_args = reload_args(); - return match unsafe { - pyre_interpreter::builtin_code_call(code as pyre_object::PyObjectRef, &call_args) - } { + // `call_args` are raw positionals; a HOPELESS-arity Signature + // (`*args`, optional positional) needs `_match_signature` binding + // before the body reads its slots. `builtin_code_call` never binds, + // so route through the positional entry that does. + return match pyre_interpreter::call::builtin_code_call_positional( + code as pyre_object::PyObjectRef, + &call_args, + ) { Ok(result) if !result.is_null() => result as i64, Ok(_) => 0, Err(mut err) => { diff --git a/pyre/pyre-macros/src/lib.rs b/pyre/pyre-macros/src/lib.rs index f9c2e1441f8..3f052648cb5 100644 --- a/pyre/pyre-macros/src/lib.rs +++ b/pyre/pyre-macros/src/lib.rs @@ -221,6 +221,7 @@ fn expand_pyre_function(func: ItemFn) -> syn::Result { if let FnArg::Typed(pt) = arg { pt.attrs.retain(|a| { !a.path().is_ident("default") + && !a.path().is_ident("posonly") && !a.path().is_ident("kwonly") && !a.path().is_ident("kwargs") }); @@ -256,6 +257,7 @@ fn expand_pyre_function(func: ItemFn) -> syn::Result { // `Signature` (`None`) and keeps the raw positional fast path. let sig_fn_name = format_ident!("{}_pyre_sig", user_name); let mut sig_stmts = Vec::::new(); + let mut posonly_marked = false; let mut kwonly_marked = false; let mut raw_slice = false; for arg in user_sig.inputs.iter() { @@ -282,6 +284,10 @@ fn expand_pyre_function(func: ItemFn) -> syn::Result { raw_slice = true; continue; } + if !posonly_marked && pt.attrs.iter().any(|a| a.path().is_ident("posonly")) { + sig_stmts.push(quote! { __b.marker_posonly(); }); + posonly_marked = true; + } if !kwonly_marked && pt.attrs.iter().any(|a| a.path().is_ident("kwonly")) { sig_stmts.push(quote! { __b.marker_kwonly(); }); kwonly_marked = true;