diff --git a/Cargo.toml b/Cargo.toml index 6d189095..0aefe890 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,10 @@ std = [] coverage = [] # The wrapper that makes compiler.wasm, its exports, host imports, allocator and panic handler. runtime = [] +# `call_pure` and the host of plain values it runs the builtins on, left out of the shipped compiler.wasm. +pure = [] +# The modules behind the stable API, for the engine's own tests and tools that read them. +internals = [] [dependencies] wasm-abi = { workspace = true } diff --git a/bench/.snapshot b/bench/.snapshot index b3946114..f19736d2 100644 --- a/bench/.snapshot +++ b/bench/.snapshot @@ -2686,6 +2686,7 @@ "5c2bc10687c51e47": [158905, 0.003931], "5c45dab6255cdfa2": [42115, 0.001092], "5c484bd4415e87a0": [30093, 0.000394], + "5c550b1fd0ac29f4": [514947, 0.010326], "5c553e7280389d16": [142059, 0.003278], "5c5b98a9293a3093": [202652335, 0.063319], "5c6bba93ffa792e3": [74334, 0.001504], @@ -3322,6 +3323,7 @@ "719fa45dde307837": [487645, 0.011391], "71be5632996ef365": [278527, 0.003983], "71d4dd885f926195": [6346730, 0.044801], + "71db7ceb1f6b7aff": [1057567, 0.022209], "71e0951cfc128284": [61225, 0.002116], "71f1ddd6928eae89": [110808, 0.003214], "71f1e207d8e1ab23": [50254, 0.001346], @@ -5021,6 +5023,7 @@ "a91ec33184d188a0": [172939, 0.002736], "a92177c469ced6d4": [286298, 0.005194], "a927435723c24ff4": [248567, 0.003412], + "a92c52d45c21a7a7": [107588, 0.003145], "a95987f5c0837746": [132760, 0.004216], "a96152270e64d52e": [32115, 0.000807], "a962501fe4ae6b5f": [146054, 0.003088], @@ -5712,6 +5715,7 @@ "c0dfa38aeb9f308f": [120868, 0.003118], "c0e0e77501467125": [77142, 0.000987], "c0ed36f21cebbc25": [154514, 0.002519], + "c0ef596fcfae5153": [633459, 0.012285], "c0f86155cec5ada9": [29542, 0.000395], "c0fea17c36a3bf71": [5658774, 0.195423], "c106b469b7ce6544": [143367, 0.002017], @@ -6789,7 +6793,7 @@ "e6e646fd2a4a0c16": [109598435, 0.013537], "e6e8e751bdb50cbd": [196511, 0.003984], "e7100f1cde6213a8": [39474, 0.000971], - "e71f49444f48944b": [730041027, 0.000408], + "e71f49444f48944b": [660041070, 0.000408], "e72c517833d63d53": [265765, 0.005015], "e732d3870c2eede9": [84194, 0.000616], "e73c84fcabca5c19": [61378, 0.000781], diff --git a/cli/src/actor/actor.rs b/cli/src/actor/actor.rs index 53673fda..e1921890 100644 --- a/cli/src/actor/actor.rs +++ b/cli/src/actor/actor.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use std::sync::{Arc, Mutex}; use compiler::modules::dir_of; -use compiler::vm::Limits; +use compiler::embed::Limits; use crate::host::{driver, now_ns, Host, Project, Sink, Status, Vm, TICK_NS}; use crate::pack::{base64_decode, Bundle, BUNDLE_TAG}; diff --git a/cli/src/actor/config.rs b/cli/src/actor/config.rs index 057957bb..bb4ce6eb 100644 --- a/cli/src/actor/config.rs +++ b/cli/src/actor/config.rs @@ -1,4 +1,4 @@ -use compiler::vm::Limits; +use compiler::embed::Limits; // The whole pool, groups plus the runtime knobs the CLI resolved from actor.yml. pub struct ActorConfig { diff --git a/cli/src/actor/scheduler.rs b/cli/src/actor/scheduler.rs index 3d4e2880..1fbdf68e 100644 --- a/cli/src/actor/scheduler.rs +++ b/cli/src/actor/scheduler.rs @@ -7,7 +7,7 @@ use std::time::{Duration, Instant}; use slab::Slab; -use compiler::vm::Limits; +use compiler::embed::Limits; use crate::host::{now_ns, Host, Instance, Project, Runtime, Vm}; diff --git a/cli/src/cmd/actor.rs b/cli/src/cmd/actor.rs index 252f2c5e..5eb7354b 100644 --- a/cli/src/cmd/actor.rs +++ b/cli/src/cmd/actor.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Context, Result}; use crate::actor::{Group, Message, Out, ActorConfig}; use crate::host::RunLimits; -use compiler::vm::Limits; +use compiler::embed::Limits; use serde::Deserialize; use std::path::Path; diff --git a/cli/src/host/mod.rs b/cli/src/host/mod.rs index 9a40eb49..ebc9b771 100644 --- a/cli/src/host/mod.rs +++ b/cli/src/host/mod.rs @@ -12,7 +12,7 @@ pub use resolver::{fetch_cached, Project}; pub use vm::{Completion, Instance, Status, Vm}; use anyhow::{anyhow, Result}; -use compiler::vm::types::Limits; +use compiler::value::Limits; use std::cell::RefCell; use std::collections::HashMap; use std::path::PathBuf; diff --git a/cli/src/host/vm.rs b/cli/src/host/vm.rs index 8f82972a..1976cca6 100644 --- a/cli/src/host/vm.rs +++ b/cli/src/host/vm.rs @@ -1,7 +1,7 @@ use super::{read, resolver, rt, stage, unstage, wt, Exports, Host, MemoryCap, Project, Sink, State}; use anyhow::{anyhow, bail, Result}; use compiler::abi::WireValue; -use compiler::vm::Limits; +use compiler::embed::Limits; use std::cell::{RefCell, RefMut}; use std::collections::{HashMap, VecDeque}; use std::rc::Rc; diff --git a/docs/03-platforms/04-rust.mdx b/docs/03-platforms/04-rust.mdx index cbe166e2..09c3cc15 100644 --- a/docs/03-platforms/04-rust.mdx +++ b/docs/03-platforms/04-rust.mdx @@ -44,6 +44,41 @@ A wasm that links the crate as a library, such as a plugin that only parses, dro edge-python = { git = "https://github.com/dylan-sutton-chavez/edge-python", tag = "v0.9.6", default-features = false } ``` +### The crate API + +The crate exposes these modules and keeps the rest to itself. + +| Module | Holds | +|---|---| +| `code` | `compile`, which lexes, parses and folds a source into an `SSAChunk`, the `Parser` for a resolver or a path, and the bytecode it makes, `Instruction`, `OpCode` with `is_jump` and `ALL`, `Value`, `Diagnostic` and `unpack_call` for the counts of a call operand. `Program::new(&chunk)` gives `scope_of(body)`, `params_of(fi)` and `bindings_of(body)` without running anything | +| `types` | `BUILTIN_TYPES`, `builtin_type`, `exc_parents`, the method table `ALL_METHODS` with `lookup_method`, `CLASS_DUNDERS` with `class_dunder` for the special names a class body may define, and `NativeFnId` with the arity of each builtin | +| `embed` | `VM` to run a chunk, `Limits`, `SchedulerStatus`, the snapshot calls `save`, `restore`, `source_of`, `limits_of`, `inspect_globals` and `inspect_stack`, and `Resolver`, `Resolved` and `NativeBinding` for modules and natives | +| `value` | `Val`, `HeapObj`, `HeapPool`, `VmErr`, `BigInt`, `as_i128`, the allocator `meter` and the `Host` trait | +| `pure` | `call_pure` with the `pure` feature on, `Host` always | +| `lexer` | `lex`, `Token` and `TokenType` | +| `modules`, `abi`, `util::sha256` | Manifests, locks and bundles, the wire format of plugins, and SHA-256 | + +`bindings_of(body)` answers for the module, every class and every function body with a `BodyKind` and a `Binding` per name slot. A function's names are `Local`, `Cell` or `Global`, the same answer `scope_of` gives. A class body's own names are `Member`, which its methods and comprehensions don't see, so they read past the class to the function around it or the module. Its other names are a `Cell` of that function, a `Lent` plain local of the function that defines the class, read by value as the class is made, or `Global`. Slots no variable takes, such as attribute names, are `Local`. + +A class body that defines a dunder outside `CLASS_DUNDERS` fails to compile. The list holds names no builtin type has as a method, such as `__set__`, `__delete__`, `__set_name__` and `__slots__`, so `ALL_METHODS` is no list of them. + +The `internals` feature opens the modules behind these for the engine's own tests and tools. They change with every release. + +### Builtins without a VM + +`call_pure` runs a builtin on plain values and returns its result or the exception it raised, with the class and the message the VM gives. No lexer, parser or VM runs, and the same body the VM runs gives the answer. It is behind the `pure` feature, so the shipped `compiler.wasm` carries none of it. + +```rust +use compiler::pure::{Plain, call_pure}; + +let up = call_pure("str.upper", &[Plain::Str("abc".into())]); +assert_eq!(up, Ok(Plain::Str("ABC".into()))); +``` + +A method is named `type.name` with its receiver first, and a dunder of a builtin value such as `str.__getitem__` resolves among the operators its type answers. `Plain` holds what a call takes and gives, ints of any size, floats, bools, `None`, `str`, `bytes`, `bytearray`, lists, tuples, dicts, sets, ranges and slices. A method or builtin function the tables mark pure runs, and any other answers `NotImplementedError`. `MethodDesc::excluded` and `NativeFnId::excluded` give the reason, such as a method that changes its receiver or one that may run user code. + +The bodies of the builtins are generic over `Host`, the heap they read and the user code only a VM runs. The VM is one host and `PlainHost` the other, so each builtin has one body and the VM compiles to the same code it did before. + ## Imports `compiler.wasm` declares five imports from `env`. diff --git a/docs/05-internals/01-design.mdx b/docs/05-internals/01-design.mdx index f70080d0..94624066 100644 --- a/docs/05-internals/01-design.mdx +++ b/docs/05-internals/01-design.mdx @@ -106,6 +106,10 @@ A job keeps between calls whatever the builtin needs, so new state goes in a var Debug and coverage builds count the loops running in `VM::loops`. `exec_in` is the only way into `exec_frame`, and a loop opening inside another panics. The test `old_path_ceilings` runs every case of `vm.json` and fails when a generic frame is pushed. Release builds have no counter. +## One body per builtin + +The methods and builtins read their values through `value::Host`, a trait over the heap and the few things only a VM has, its stack for results, its op budget, and the user iterators, dunders and exception state it keeps. `impl Host for VM` answers with the VM's own fields and inlines, so the VM compiles each body to the code it had before. `PlainHost` holds a heap with the kinds of Python and no user code, and `call_pure` runs a builtin on it. An entry of `ALL_METHODS` marked `pure` builds both forms of its body, and `MethodDesc::excluded` says why an entry has no plain form. The test `every_method_is_pure_or_says_why_not` keeps the table complete as builtins are added. + ## Memory model `Val` is 64 bits and NaN-boxed, with `QNAN = 0x7FFC_0000_0000_0000` and `SIGN = 0x8000...`. diff --git a/fuzz/src/main.rs b/fuzz/src/main.rs index 7edae9bc..67cde5c0 100644 --- a/fuzz/src/main.rs +++ b/fuzz/src/main.rs @@ -1,10 +1,10 @@ use afl::fuzz; use compiler::lexer::lex; -use compiler::parser::Parser; -use compiler::vm::snapshot; -use compiler::vm::types::{SchedulerStatus, VmErr}; -use compiler::vm::{Limits, VM}; +use compiler::code::Parser; +use compiler::embed as snapshot; +use compiler::value::{SchedulerStatus, VmErr}; +use compiler::embed::{Limits, VM}; // Every run meters what it allocates, as compiler.wasm does. #[global_allocator] diff --git a/lang/src/lib.rs b/lang/src/lib.rs index ce8ad255..babf738c 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -7,11 +7,11 @@ use core::cell::RefCell; use compiler::lexer::{lex, TokenType}; use compiler::modules::{NativeBinding, Resolved, Resolver}; -use compiler::parser::{Diagnostic, Parser, SSAChunk}; +use compiler::code::{Diagnostic, Parser, SSAChunk}; use compiler::value::{HeapObj, HeapPool, Val, VmErr, as_i128}; -use compiler::vm::VM; +use compiler::embed::VM; -pub use compiler::vm::Limits; +pub use compiler::embed::Limits; // a source rewrite applied before lexing, its output must be valid edge python pub type Transform = dyn Fn(&str) -> String + Send + Sync + 'static; @@ -242,7 +242,7 @@ impl Engine { } return Err(Error::Compile(buf, errs.first().map(|e| line_col(&src, e.start)))); } - compiler::optimizer::constant_fold(&mut chunk); + compiler::code::constant_fold(&mut chunk); Ok(Program { chunk, source: src, limits: self.limits }) } } diff --git a/src/bridge.rs b/src/bridge.rs index 177ed3ab..7563fb67 100644 --- a/src/bridge.rs +++ b/src/bridge.rs @@ -1,7 +1,7 @@ #![allow(clippy::missing_safety_doc)] // safety contract in docs/03-reference/06-abi.mdx, per-fn sections would duplicate it use crate::abi::{classify_decode, classify_encode, DecodeBits, EncodeRequest, ErrorKind, ErrorStash, HandleTable, Op, PrimitiveBytes, TAG_INVALID}; -use crate::vm::types::{DictMap, HeapObj, Val, VmErr}; +use crate::vm::types::{DictMap, HeapObj, Host, Val, VmErr}; use crate::vm::methods::lookup_method; use crate::vm::VM; use alloc::{rc::Rc, string::{String, ToString}, vec::Vec}; @@ -202,7 +202,7 @@ fn host_op(op: u32, recv: u32, name: &str, args: &[Val]) -> Result { Some(Op::NewDict) => in_vm("edge_op new_dict called outside run()", |vm| vm.heap.alloc(HeapObj::Dict(Rc::new(RefCell::new(DictMap::new()))))), Some(Op::NewList) => in_vm("edge_op new_list called outside run()", |vm| vm.heap.alloc(HeapObj::List(Rc::new(RefCell::new(Vec::new()))))), Some(Op::TypeOf) => dispatch_type_of(recv), - Some(Op::NewTuple) => in_vm("edge_op new_tuple called outside run()", |vm| vm.tuple_from_items(args.to_vec())), + Some(Op::NewTuple) => in_vm("edge_op new_tuple called outside run()", |vm| vm.heap.alloc(HeapObj::Tuple(args.to_vec()))), Some(Op::NewSet) => in_vm("edge_op new_set called outside run()", |vm| vm.set_from_items(args.to_vec())), Some(Op::NewFrozenSet) => in_vm("edge_op new_frozenset called outside run()", |vm| vm.frozenset_from_items(args.to_vec())), // A system call names its plugin call in `recv`, the id `edge_call_id` gave it. diff --git a/src/frontend/int.rs b/src/frontend/int.rs index 68f76a71..776bb040 100644 --- a/src/frontend/int.rs +++ b/src/frontend/int.rs @@ -401,6 +401,8 @@ pub fn range_slice(heap: &mut HeapPool, v: Val, start: Val, stop: Val, step: Val /* A slice index with a boxed int clamped to the inline extreme of its sign, which no sequence length reaches. */ pub fn clip_index(heap: &HeapPool, v: Val) -> Val { let v = crate::value::unboxed(v, heap); + // A bool bounds as the int it is. + if v.is_bool() { return Val::int(v.as_bool() as i64); } if !is_big(heap, v) { return v; } Val::int(if view(heap, v).is_some_and(|b| b.is_neg()) { Val::INT_MIN } else { Val::INT_MAX }) } diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs index de3f8494..7ff49ec3 100644 --- a/src/frontend/mod.rs +++ b/src/frontend/mod.rs @@ -3,3 +3,10 @@ pub mod bigint; pub mod complex; pub mod sentinel; pub mod int; + +// Wide ints register first so no constant boxes one before them. +pub fn install(heap: &mut crate::value::HeapPool) { + heap.set_wide_int(&int::KIND); + heap.know_kind(&complex::KIND); + heap.know_kind(&sentinel::KIND); +} diff --git a/src/lib.rs b/src/lib.rs index 437da46c..0d610395 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,12 +13,12 @@ pub mod wasm; /* Internal compiler helpers (not Edge Python stdlib), separate from pipeline code. */ pub mod util { - pub mod hash; - pub mod fstr; - pub mod jesc; + pub(crate) mod hash; + pub(crate) mod fstr; + pub(crate) mod jesc; pub mod sha256; - pub mod uni; - pub mod suggest; + pub(crate) mod uni; + pub(crate) mod suggest; /* `Box::leak` for memory kept until exit, which Miri then holds as a static root instead of reporting it. */ pub fn leak(boxed: alloc::boxed::Box) -> &'static mut T { @@ -38,11 +38,78 @@ pub mod util { /* NaN-boxed values and the mark-and-sweep heap, the layer both the frontend and the VM build on. */ pub mod value; +// Public only through the internals feature for the engine's own tests and tools. +macro_rules! internal { + ($($(#[$a:meta])* $m:ident;)*) => { $( + $(#[$a])* #[cfg(feature = "internals")] pub mod $m; + $(#[$a])* #[cfg(not(feature = "internals"))] pub(crate) mod $m; + )* }; +} + pub mod lexer; -pub mod parser; -/* Post-SSA passes, run between parse and boot, touches no VM state. */ -pub mod optimizer; -pub mod vm; -/* The kinds Edge Python registers on the engine, the wide ints first. */ -pub mod frontend; +internal! { + parser; + /* Post-SSA passes, run between parse and boot, touches no VM state. */ + optimizer; + vm; + /* The kinds Edge Python registers on the engine, the wide ints first. */ + frontend; +} +pub mod pure; pub mod modules; + +// Source to bytecode and the facts of compiled code read without running it. +pub mod code { + pub use crate::parser::{ + Parser, SSAChunk, Instruction, OpCode, Value, ImportKind, ImportEntry, Diagnostic, Lines, SsaName, ssa_strip, param_base_name, unpack_call, + COMP_ARG, SPREAD_BASES, INPLACE, ITER_VALUE, ITER_AS_IS, ITER_ASYNC, YIELD_FROM, SPREAD_ARGS, KEYWORDS, + }; + pub use crate::optimizer::constant_fold; + pub use crate::vm::{Binding, BodyKind, FnScope, Kind, ParamKind}; + + // Lexes, parses and folds `src` into a chunk and its errors. + pub fn compile(src: &str) -> (SSAChunk, alloc::vec::Vec) { + let (tokens, _) = crate::lexer::lex(src); + let (mut chunk, errors) = Parser::new(src, tokens.into_iter()).parse(); + constant_fold(&mut chunk); + (chunk, errors) + } + + // Each body's scope and parameters resolved as the VM resolves them without running. + pub struct Program<'c> { vm: crate::vm::VM<'c>, bodies: crate::util::hash::FxHashMap<*const SSAChunk, (BodyKind, alloc::vec::Vec)> } + + impl<'c> Program<'c> { + pub fn new(chunk: &'c SSAChunk) -> Self { + let mut vm = crate::vm::VM::new(chunk); + let bodies = vm.body_bindings(chunk); + Self { vm, bodies } + } + // None for the module body. + pub fn scope_of(&self, body: &SSAChunk) -> Option<(usize, &FnScope)> { self.vm.scope_of(body) } + // Which body `body` is and the binding of each of its names, a module's, a class's or a function's, None for a chunk of another program. + pub fn bindings_of(&self, body: &SSAChunk) -> Option<(BodyKind, &[Binding])> { self.bodies.get(&(body as *const SSAChunk)).map(|(k, b)| (*k, b.as_slice())) } + // Kind and slot of each parameter in order. + pub fn params_of(&self, fi: usize) -> &[(ParamKind, usize)] { self.vm.params_of(fi) } + } +} + +// Builtin types, methods and functions with arity and purity. +pub mod types { + pub use crate::parser::{BUILTIN_TYPES, CLASS_DUNDERS, builtin_type, class_dunder}; + pub use crate::vm::{ALL_METHODS, BuiltinMethodId, MethodDesc, MethodKind, lookup_method, exc_parents}; + pub use crate::value::NativeFnId; +} + +// A VM to embed, run and snapshot with host modules and natives. +pub mod embed { + pub use crate::vm::VM; + pub use crate::vm::snapshot::{SnapErr, save, restore, source_of, limits_of, inspect_globals, inspect_stack}; + pub use crate::value::{Limits, SchedulerStatus}; + pub use crate::modules::{NativeBinding, Resolved, Resolver}; +} + +// Unstable internals for the engine's own tests and tools. +#[cfg(feature = "internals")] +pub mod internals { + pub use crate::{frontend, optimizer, parser, vm}; +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 560057be..be420883 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -39,6 +39,9 @@ pub(super) const fn pack_call(pos: u16, kw: u16) -> u16 { ((kw & 0xFF) << 8) | (pos & 0xFF) } +// Positional and keyword counts of a call operand without its flags. +pub const fn unpack_call(operand: u16) -> (u16, u16) { (operand & 0xFF, (operand >> 8) & 0x3F) } + // Operand of a binary op written as `op=`, so the VM asks `__iop__` first. pub const INPLACE: u16 = 1; diff --git a/src/parser/types.rs b/src/parser/types.rs index 84251122..2af4c2f4 100644 --- a/src/parser/types.rs +++ b/src/parser/types.rs @@ -193,21 +193,26 @@ pub(super) fn removed_builtin(name: &str) -> Option { REMOVED_BUILTINS.iter().position(|&(n, _)| n == name) } -/* Whether a class body may bind dunder `name`, every other one compiles to an error. */ -pub(super) fn class_dunder(name: &str) -> bool { - matches!(name, - "__init__" | "__repr__" | "__str__" | "__format__" | "__bool__" | "__len__" | "__iter__" | "__next__" | - "__contains__" | "__getitem__" | "__setitem__" | "__delitem__" | "__missing__" | "__getattr__" | "__call__" | "__enter__" | - "__exit__" | "__eq__" | "__ne__" | "__lt__" | "__le__" | "__gt__" | "__ge__" | "__int__" | "__float__" | - "__index__" | "__abs__" | "__neg__" | "__pos__" | "__invert__" | "__class_getitem__" | "__match_args__" | - "__init_subclass__" | "__set_name__" | "__get__" | "__set__" | "__delete__" | "__setattr__" | "__delattr__" | "__new__" | "__slots__" | "__hash__" | "__complex__" | - "__add__" | "__sub__" | "__mul__" | "__truediv__" | "__floordiv__" | "__matmul__" | "__mod__" | "__pow__" | - "__and__" | "__or__" | "__xor__" | "__lshift__" | "__rshift__" | "__radd__" | "__rsub__" | "__rmul__" | - "__rtruediv__" | "__rfloordiv__" | "__rmatmul__" | "__rmod__" | "__rpow__" | "__rand__" | "__ror__" | - "__rxor__" | "__rlshift__" | "__rrshift__" | "__iadd__" | "__isub__" | "__imul__" | "__itruediv__" | - "__ifloordiv__" | "__imatmul__" | "__imod__" | "__ipow__" | "__iand__" | "__ior__" | "__ixor__" | - "__ilshift__" | "__irshift__" | "__dir__" | "__await__" | "__aiter__" | "__anext__" | "__aenter__" | "__aexit__" - ) +/* The dunders a class body may bind, every other one compiles to an error, `CLASS_DUNDERS` listing them and `class_dunder` testing one. */ +macro_rules! class_dunders { + ($($n:literal)*) => { + pub static CLASS_DUNDERS: &[&str] = &[$($n),*]; + pub fn class_dunder(name: &str) -> bool { matches!(name, $($n)|*) } + }; +} + +class_dunders! { + "__init__" "__repr__" "__str__" "__format__" "__bool__" "__len__" "__iter__" "__next__" + "__contains__" "__getitem__" "__setitem__" "__delitem__" "__missing__" "__getattr__" "__call__" "__enter__" + "__exit__" "__eq__" "__ne__" "__lt__" "__le__" "__gt__" "__ge__" "__int__" "__float__" + "__index__" "__abs__" "__neg__" "__pos__" "__invert__" "__class_getitem__" "__match_args__" + "__init_subclass__" "__set_name__" "__get__" "__set__" "__delete__" "__setattr__" "__delattr__" "__new__" "__slots__" "__hash__" "__complex__" + "__add__" "__sub__" "__mul__" "__truediv__" "__floordiv__" "__matmul__" "__mod__" "__pow__" + "__and__" "__or__" "__xor__" "__lshift__" "__rshift__" "__radd__" "__rsub__" "__rmul__" + "__rtruediv__" "__rfloordiv__" "__rmatmul__" "__rmod__" "__rpow__" "__rand__" "__ror__" + "__rxor__" "__rlshift__" "__rrshift__" "__iadd__" "__isub__" "__imul__" "__itruediv__" + "__ifloordiv__" "__imatmul__" "__imod__" "__ipow__" "__iand__" "__ior__" "__ixor__" + "__ilshift__" "__irshift__" "__dir__" "__await__" "__aiter__" "__anext__" "__aenter__" "__aexit__" } // Constant literals stored in the bytecode constants pool. diff --git a/src/pure/mod.rs b/src/pure/mod.rs new file mode 100644 index 00000000..9c9b0afa --- /dev/null +++ b/src/pure/mod.rs @@ -0,0 +1,6 @@ +pub use crate::value::Host; + +#[cfg(feature = "pure")] +mod plain; +#[cfg(feature = "pure")] +pub use plain::{Plain, PlainFn, PlainHost, Raised, call_pure, is_pure}; diff --git a/src/pure/plain.rs b/src/pure/plain.rs new file mode 100644 index 00000000..71118535 --- /dev/null +++ b/src/pure/plain.rs @@ -0,0 +1,189 @@ +use alloc::{rc::Rc, string::String, vec::Vec}; +use core::cell::RefCell; +use crate::frontend::bigint::BigInt; +use crate::value::{DictMap, HeapObj, HeapPool, Host, IterFrame, Limits, Val, ValSet, VmErr}; +use crate::vm::methods::{ALL_METHODS, Post, lookup_method, method_arity, post_result}; + +// Big holds only ints past i64 and sets compare without order. +#[derive(Clone, Debug)] +pub enum Plain { None, Bool(bool), Int(i64), Big(BigInt), Float(f64), Complex(f64, f64), Str(String), Bytes(Vec), ByteArray(Vec), List(Vec), Tuple(Vec), Dict(Vec<(Plain, Plain)>), FrozenDict(Vec<(Plain, Plain)>), Set(Vec), FrozenSet(Vec), Range(i64, i64, i64), Slice(alloc::boxed::Box<[Plain; 3]>) } + +impl PartialEq for Plain { + fn eq(&self, o: &Self) -> bool { + use Plain::*; + let unordered = |a: &[Plain], b: &[Plain]| a.len() == b.len() && a.iter().all(|x| b.contains(x)); + match (self, o) { + (None, None) => true, + (Bool(a), Bool(b)) => a == b, + (Int(a), Int(b)) => a == b, + (Big(a), Big(b)) => a == b, + // Two NaNs read as the same result. + (Float(a), Float(b)) => a == b || a.is_nan() && b.is_nan(), + (Complex(a, b), Complex(x, y)) => (a, b) == (x, y), + (Str(a), Str(b)) => a == b, + (Bytes(a), Bytes(b)) | (ByteArray(a), ByteArray(b)) => a == b, + (List(a), List(b)) | (Tuple(a), Tuple(b)) => a == b, + (Dict(a), Dict(b)) | (FrozenDict(a), FrozenDict(b)) => a.len() == b.len() && a.iter().all(|e| b.contains(e)), + (Range(a, b, c), Range(x, y, z)) => (a, b, c) == (x, y, z), + (Slice(a), Slice(b)) => a == b, + (Set(a), Set(b)) | (FrozenSet(a), FrozenSet(b)) => unordered(a, b), + _ => false, + } + } +} + +// Class and message as the VM renders them. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Raised { pub class: String, pub message: String } + +impl From for Raised { + fn from(e: VmErr) -> Self { Self { class: e.class_name(), message: e.message() } } +} + +pub type PlainFn = fn(&mut PlainHost, Val, &[Val]) -> Result<(), VmErr>; + +// A heap without user code so every iterable is a builtin container. +pub struct PlainHost { heap: HeapPool, stack: Vec, budget: u64 } + +impl Host for PlainHost { + #[inline] fn heap(&self) -> &HeapPool { &self.heap } + #[inline] fn heap_mut(&mut self) -> &mut HeapPool { &mut self.heap } + #[inline] fn push(&mut self, v: Val) { self.stack.push(v); } + #[inline] fn charge(&mut self, _n: usize) -> Result<(), VmErr> { Ok(()) } +} + +impl crate::vm::natives::Frames for PlainHost { + type Code = (); + fn last(&self) -> Option { self.stack.last().copied() } + fn stack(&mut self) -> &mut Vec { &mut self.stack } + fn budget_mut(&mut self) -> &mut u64 { &mut self.budget } + fn heap_budget(&mut self) -> (&HeapPool, &mut u64) { (&self.heap, &mut self.budget) } + fn pop(&mut self) -> Result { self.stack.pop().ok_or_else(|| crate::value::cold_runtime("stack underflow")) } + fn pop_n(&mut self, n: usize) -> Result, VmErr> { + let at = self.stack.len().checked_sub(n).ok_or_else(|| crate::value::cold_runtime("stack underflow"))?; + Ok(self.stack.split_off(at)) + } +} + +impl Default for PlainHost { + fn default() -> Self { + let mut heap = HeapPool::new(Limits::sandbox().memory); + crate::frontend::install(&mut heap); + Self { heap, stack: Vec::new(), budget: Limits::sandbox().ops } + } +} + +impl PlainHost { + fn val(&mut self, p: &Plain) -> Result { + let items = |h: &mut Self, xs: &[Plain]| xs.iter().map(|x| h.val(x)).collect::, _>>(); + Ok(match p { + Plain::None => Val::none(), + Plain::Bool(b) => Val::bool(*b), + Plain::Int(i) => self.heap.int(*i as i128)?, + Plain::Big(b) => crate::frontend::int::val(&mut self.heap, b.clone())?, + Plain::Float(f) => Val::float(*f), + &Plain::Complex(re, im) => crate::frontend::complex::val(&mut self.heap, re, im)?, + Plain::Str(s) => self.heap.alloc(HeapObj::Str(s.clone()))?, + Plain::Bytes(b) => self.heap.alloc(HeapObj::Bytes(b.clone()))?, + Plain::ByteArray(b) => self.heap.alloc(HeapObj::ByteArray(b.clone()))?, + Plain::List(xs) => { let v = items(self, xs)?; self.alloc_list(v)? } + Plain::Tuple(xs) => { let v = items(self, xs)?; self.heap.alloc(HeapObj::Tuple(v))? } + Plain::Dict(es) | Plain::FrozenDict(es) => { + let mut d = DictMap::with_capacity(es.len()); + for (k, v) in es { let (k, v) = (self.val(k)?, self.val(v)?); d.insert(k, v, &self.heap); } + let d = Rc::new(RefCell::new(d)); + self.heap.alloc(if matches!(p, Plain::Dict(_)) { HeapObj::Dict(d) } else { HeapObj::FrozenDict(d) })? + } + &Plain::Range(a, b, c) => self.heap.alloc(HeapObj::Range(a, b, c))?, + Plain::Slice(parts) => { let [a, b, c] = &**parts; let (a, b, c) = (self.val(a)?, self.val(b)?, self.val(c)?); self.heap.alloc(HeapObj::Slice(a, b, c))? } + Plain::Set(xs) => { let v = items(self, xs)?; let s = ValSet::from_vals(&v, &self.heap); self.heap.alloc(HeapObj::Set(Rc::new(RefCell::new(s))))? } + Plain::FrozenSet(xs) => { let v = items(self, xs)?; let s = ValSet::from_vals(&v, &self.heap); self.heap.alloc(HeapObj::FrozenSet(Rc::new(s)))? } + }) + } + + fn plain(&mut self, v: Val) -> Result { + if v.is_none() { return Ok(Plain::None); } + if v.is_bool() { return Ok(Plain::Bool(v.as_bool())); } + if v.is_int() { return Ok(Plain::Int(v.as_int())); } + if v.is_float() { return Ok(Plain::Float(v.as_float())); } + if crate::frontend::complex::is_complex(&self.heap, v) && let Some(z) = crate::frontend::complex::parts(&self.heap, v) { return Ok(Plain::Complex(z.re, z.im)); } + if let Some(b) = crate::frontend::int::of_val(&self.heap, v) { return Ok(b.to_i128().and_then(|i| i64::try_from(i).ok()).map_or(Plain::Big(b), Plain::Int)); } + Ok(match self.heap.try_get(v) { + Some(HeapObj::Str(s)) => Plain::Str(s.clone()), + Some(HeapObj::Bytes(b)) => Plain::Bytes(b.clone()), + Some(HeapObj::ByteArray(b)) => Plain::ByteArray(b.clone()), + Some(HeapObj::List(l)) => { let xs = l.borrow().clone(); Plain::List(self.all(&xs)?) } + Some(HeapObj::Tuple(t)) => { let xs = t.clone(); Plain::Tuple(self.all(&xs)?) } + Some(HeapObj::Dict(d)) => { let es: Vec<(Val, Val)> = d.borrow().iter().collect(); Plain::Dict(self.pairs(&es)?) } + Some(HeapObj::FrozenDict(d)) => { let es: Vec<(Val, Val)> = d.borrow().iter().collect(); Plain::FrozenDict(self.pairs(&es)?) } + Some(&HeapObj::Range(a, b, c)) => Plain::Range(a, b, c), + Some(&HeapObj::Slice(a, b, c)) => Plain::Slice(alloc::boxed::Box::new([self.plain(a)?, self.plain(b)?, self.plain(c)?])), + // A builtin iterator reads to its end as a list. + Some(HeapObj::Iter(frame, _)) if matches!(&*frame.borrow(), IterFrame::Seq { .. } | IterFrame::Watched { .. } | IterFrame::List { .. } | IterFrame::ListRev { .. } + | IterFrame::Range { .. } | IterFrame::BigRange { .. } | IterFrame::DictWalk { .. } | IterFrame::Zip { .. } | IterFrame::Enumerate { .. }) => { + let mut xs = Vec::new(); + while let Some(x) = crate::vm::natives::Frames::next_of(self, v, &())? { xs.push(x); } + Plain::List(self.all(&xs)?) + } + Some(HeapObj::Set(s)) => { let xs: Vec = s.borrow().iter().copied().collect(); Plain::Set(self.all(&xs)?) } + Some(HeapObj::FrozenSet(s)) => { let xs: Vec = s.iter().copied().collect(); Plain::FrozenSet(self.all(&xs)?) } + _ => return Err(VmErr::TypeMsg(crate::s!("a '", str self.type_name(v), "' is not a plain value"))), + }) + } + + fn all(&mut self, xs: &[Val]) -> Result, VmErr> { xs.iter().map(|&x| self.plain(x)).collect() } + + fn pairs(&mut self, es: &[(Val, Val)]) -> Result, VmErr> { es.iter().map(|&(k, v)| Ok((self.plain(k)?, self.plain(v)?))).collect() } + + // A method is `type.name` with its receiver first. + pub fn call(&mut self, f: &str, args: &[Plain]) -> Result { + let Some((ty, name)) = f.split_once('.') else { return self.call_native(f, args) }; + let id = method_of(ty, name).ok_or_else(|| not_pure(f))?; + let m = &ALL_METHODS[id.raw() as usize]; + let body = m.plain.ok_or_else(|| not_pure(f))?; + // Methods the type answers itself take no receiver. + let (recv, rest) = if matches!(name, "maketrans" | "fromkeys" | "fromhex" | "from_bytes" | "from_number") { (Val::none(), args) } else { + let (recv, rest) = args.split_first().ok_or_else(|| Raised::from(VmErr::TypeMsg(crate::s!("unbound method ", str ty, ".", str name, "() needs an argument"))))?; + let recv = self.val(recv)?; + if self.type_name(recv) != ty { + return Err(VmErr::TypeMsg(crate::s!("descriptor '", str name, "' for '", str ty, "' objects doesn't apply to a '", str self.type_name(recv), "' object")).into()); + } + (recv, rest) + }; + let pos = rest.iter().map(|a| self.val(a)).collect::, _>>()?; + method_arity(id, pos.len())?; + body(self, recv, &pos)?; + let out = self.stack.pop().ok_or_else(|| not_pure(f))?; + let out = if m.post == Post::None { out } else { post_result(self, m.post, out)? }; + Ok(self.plain(out)?) + } + + // Arguments go on the stack as a call pushes them. + fn call_native(&mut self, f: &str, args: &[Plain]) -> Result { + let id = crate::value::NativeFnId::from_name(f).filter(|id| id.pure()).ok_or_else(|| not_pure(f))?; + let n = args.len() as u16; + if !id.takes(n) { return Err(crate::vm::opcodes::function::arity_err(id, n).into()); } + for a in args { let v = self.val(a)?; self.stack.push(v); } + id.plain(self, n).ok_or_else(|| not_pure(f))??; + let out = self.stack.pop().ok_or_else(|| not_pure(f))?; + Ok(self.plain(out)?) + } +} + +// A dunder of a builtin value resolves among the operators its type answers. +fn method_of(ty: &str, name: &str) -> Option { + lookup_method(ty, name).or_else(|| crate::vm::methods::operator::applies(ty, name).then(|| lookup_method("op", name)).flatten()) +} + +fn not_pure(f: &str) -> Raised { Raised { class: "NotImplementedError".into(), message: crate::s!(str f, " has no pure form") } } + +// Whether call_pure runs builtin `f`. +pub fn is_pure(f: &str) -> bool { + match f.split_once('.') { + Some((ty, name)) => method_of(ty, name).is_some_and(|id| ALL_METHODS[id.raw() as usize].plain.is_some()), + None => crate::value::NativeFnId::from_name(f).is_some_and(|id| id.pure()), + } +} + +// The same body the VM runs with no VM, lexer or parser. +pub fn call_pure(f: &str, args: &[Plain]) -> Result { PlainHost::default().call(f, args) } diff --git a/src/util/uni.rs b/src/util/uni.rs index 3f9dbe4b..7a036fb8 100644 --- a/src/util/uni.rs +++ b/src/util/uni.rs @@ -341,6 +341,28 @@ static TITLE: [(u32, &str); 135] = [ (0xFB15, "\u{544}\u{56B}"), (0xFB16, "\u{54E}\u{576}"), (0xFB17, "\u{544}\u{56D}"), ]; +// Folds that differ from lowercase as offset runs and grown strings ending in FOLDS. +static FOLD_RUNS: [(u16, u16, i32); 24] = [ + (0xB5, 0xB5, 775), (0x17F, 0x17F, -268), (0x345, 0x345, 116), (0x3C2, 0x3C2, 1), (0x3D0, 0x3D0, -30), (0x3D1, 0x3D1, -25), + (0x3D5, 0x3D5, -15), (0x3D6, 0x3D6, -22), (0x3F0, 0x3F0, -54), (0x3F1, 0x3F1, -48), (0x3F5, 0x3F5, -64), (0x13A0, 0x13F5, 0), + (0x13F8, 0x13FD, -8), (0x1C80, 0x1C80, -6222), (0x1C81, 0x1C81, -6221), (0x1C82, 0x1C82, -6212), (0x1C83, 0x1C84, -6210), (0x1C85, 0x1C85, -6211), + (0x1C86, 0x1C86, -6204), (0x1C87, 0x1C87, -6180), (0x1C88, 0x1C88, 35267), (0x1E9B, 0x1E9B, -58), (0x1FBE, 0x1FBE, -7173), (0xAB70, 0xABBF, -38864), +]; +static FOLD: [(u16, u16); 103] = [ + (0xDF, 2), (0x149, 5), (0x1F0, 8), (0x390, 14), (0x3B0, 20), (0x587, 24), (0x1E96, 27), (0x1E97, 30), (0x1E98, 33), (0x1E99, 36), + (0x1E9A, 39), (0x1E9E, 41), (0x1F50, 45), (0x1F52, 51), (0x1F54, 57), (0x1F56, 63), (0x1F80, 68), (0x1F81, 73), (0x1F82, 78), (0x1F83, 83), + (0x1F84, 88), (0x1F85, 93), (0x1F86, 98), (0x1F87, 103), (0x1F88, 108), (0x1F89, 113), (0x1F8A, 118), (0x1F8B, 123), (0x1F8C, 128), (0x1F8D, 133), + (0x1F8E, 138), (0x1F8F, 143), (0x1F90, 148), (0x1F91, 153), (0x1F92, 158), (0x1F93, 163), (0x1F94, 168), (0x1F95, 173), (0x1F96, 178), (0x1F97, 183), + (0x1F98, 188), (0x1F99, 193), (0x1F9A, 198), (0x1F9B, 203), (0x1F9C, 208), (0x1F9D, 213), (0x1F9E, 218), (0x1F9F, 223), (0x1FA0, 228), (0x1FA1, 233), + (0x1FA2, 238), (0x1FA3, 243), (0x1FA4, 248), (0x1FA5, 253), (0x1FA6, 258), (0x1FA7, 263), (0x1FA8, 268), (0x1FA9, 273), (0x1FAA, 278), (0x1FAB, 283), + (0x1FAC, 288), (0x1FAD, 293), (0x1FAE, 298), (0x1FAF, 303), (0x1FB2, 308), (0x1FB3, 312), (0x1FB4, 316), (0x1FB6, 320), (0x1FB7, 326), (0x1FBC, 330), + (0x1FC2, 335), (0x1FC3, 339), (0x1FC4, 343), (0x1FC6, 347), (0x1FC7, 353), (0x1FCC, 357), (0x1FD2, 363), (0x1FD3, 369), (0x1FD6, 373), (0x1FD7, 379), + (0x1FE2, 385), (0x1FE3, 391), (0x1FE4, 395), (0x1FE6, 399), (0x1FE7, 405), (0x1FF2, 410), (0x1FF3, 414), (0x1FF4, 418), (0x1FF6, 422), (0x1FF7, 428), + (0x1FFC, 432), (0xFB00, 434), (0xFB01, 436), (0xFB02, 438), (0xFB03, 441), (0xFB04, 444), (0xFB05, 446), (0xFB06, 448), (0xFB13, 452), (0xFB14, 456), + (0xFB15, 460), (0xFB16, 464), (0xFB17, 468), +]; +static FOLDS: &str = "ss\u{2BC}nj\u{30C}\u{3B9}\u{308}\u{301}\u{3C5}\u{308}\u{301}\u{565}\u{582}h\u{331}t\u{308}w\u{30A}y\u{30A}a\u{2BE}ss\u{3C5}\u{313}\u{3C5}\u{313}\u{300}\u{3C5}\u{313}\u{301}\u{3C5}\u{313}\u{342}\u{1F00}\u{3B9}\u{1F01}\u{3B9}\u{1F02}\u{3B9}\u{1F03}\u{3B9}\u{1F04}\u{3B9}\u{1F05}\u{3B9}\u{1F06}\u{3B9}\u{1F07}\u{3B9}\u{1F00}\u{3B9}\u{1F01}\u{3B9}\u{1F02}\u{3B9}\u{1F03}\u{3B9}\u{1F04}\u{3B9}\u{1F05}\u{3B9}\u{1F06}\u{3B9}\u{1F07}\u{3B9}\u{1F20}\u{3B9}\u{1F21}\u{3B9}\u{1F22}\u{3B9}\u{1F23}\u{3B9}\u{1F24}\u{3B9}\u{1F25}\u{3B9}\u{1F26}\u{3B9}\u{1F27}\u{3B9}\u{1F20}\u{3B9}\u{1F21}\u{3B9}\u{1F22}\u{3B9}\u{1F23}\u{3B9}\u{1F24}\u{3B9}\u{1F25}\u{3B9}\u{1F26}\u{3B9}\u{1F27}\u{3B9}\u{1F60}\u{3B9}\u{1F61}\u{3B9}\u{1F62}\u{3B9}\u{1F63}\u{3B9}\u{1F64}\u{3B9}\u{1F65}\u{3B9}\u{1F66}\u{3B9}\u{1F67}\u{3B9}\u{1F60}\u{3B9}\u{1F61}\u{3B9}\u{1F62}\u{3B9}\u{1F63}\u{3B9}\u{1F64}\u{3B9}\u{1F65}\u{3B9}\u{1F66}\u{3B9}\u{1F67}\u{3B9}\u{1F70}\u{3B9}\u{3B1}\u{3B9}\u{3AC}\u{3B9}\u{3B1}\u{342}\u{3B1}\u{342}\u{3B9}\u{3B1}\u{3B9}\u{1F74}\u{3B9}\u{3B7}\u{3B9}\u{3AE}\u{3B9}\u{3B7}\u{342}\u{3B7}\u{342}\u{3B9}\u{3B7}\u{3B9}\u{3B9}\u{308}\u{300}\u{3B9}\u{308}\u{301}\u{3B9}\u{342}\u{3B9}\u{308}\u{342}\u{3C5}\u{308}\u{300}\u{3C5}\u{308}\u{301}\u{3C1}\u{313}\u{3C5}\u{342}\u{3C5}\u{308}\u{342}\u{1F7C}\u{3B9}\u{3C9}\u{3B9}\u{3CE}\u{3B9}\u{3C9}\u{342}\u{3C9}\u{342}\u{3B9}\u{3C9}\u{3B9}fffiflffifflstst\u{574}\u{576}\u{574}\u{565}\u{574}\u{56B}\u{57E}\u{576}\u{574}\u{56D}"; + // The range of `table` holding `c`, the tables are sorted and disjoint. fn find(table: &[(u32, u32)], c: char) -> Option { use core::cmp::Ordering::*; @@ -386,6 +408,20 @@ pub fn push_title(out: &mut alloc::string::String, c: char) { } } +// Full casefold so ß gives ss. +pub fn push_fold(out: &mut alloc::string::String, c: char) { + if c.is_ascii() { return out.push(c.to_ascii_lowercase()); } + let n = c as u32; + if let Ok(i) = FOLD.binary_search_by_key(&n, |&(k, _)| k as u32) { + let start = if i == 0 { 0 } else { FOLD[i - 1].1 as usize }; + return out.push_str(&FOLDS[start..FOLD[i].1 as usize]); + } + match FOLD_RUNS.iter().find(|&&(lo, hi, _)| (lo as u32..=hi as u32).contains(&n)) { + Some(&(_, _, k)) => out.extend(char::from_u32(n.wrapping_add_signed(k))), + None => c.to_lowercase().for_each(|l| out.push(l)), + } +} + // A titlecase letter such as `Dž`, neither upper nor lower but its own titlecase. pub fn is_titlecase(c: char) -> bool { !c.is_lowercase() && !c.is_uppercase() diff --git a/src/value/host.rs b/src/value/host.rs new file mode 100644 index 00000000..1d6719f3 --- /dev/null +++ b/src/value/host.rs @@ -0,0 +1,1039 @@ +use alloc::{string::{String, ToString}, vec, vec::Vec}; +use crate::s; +use core::cmp::Ordering; +use super::*; +use super::render::{MAX_REPR_LEN, RENDER_DEPTH_MAX, at, format_bytes, quote_bytes, repr_str}; + +// What a builtin body reads past the heap so one body serves two hosts. +pub trait Host { + fn heap(&self) -> &HeapPool; + fn heap_mut(&mut self) -> &mut HeapPool; + // Hands back the result of a call. + fn push(&mut self, v: Val); + // Pays native work against the op budget. + fn charge(&mut self, n: usize) -> Result<(), VmErr>; + // Items only the VM can step and none for a builtin container. + fn vm_items(&mut self, _v: Val) -> Result>, VmErr> { Ok(None) } + fn type_name(&self, v: Val) -> &str { self.heap().type_name(v) } + // Keeps `v` alive until unroot lets it go. + fn root(&mut self, _v: Val) {} + fn unroot(&mut self, _n: usize) {} + // Runs `f` with `vals` kept alive. + fn with_roots(&mut self, vals: impl IntoIterator, f: impl FnOnce(&mut Self) -> R) -> R where Self: Sized { + let mut n = 0; + for v in vals { self.root(v); n += 1; } + let r = f(self); + self.unroot(n); + r + } + + // Text of functions, classes, instances and coroutines only the VM makes. + fn display_other(&self, v: Val, _seen: &mut Vec) -> String { s!("<", str self.type_name(v), " object", str &at(v), ">") } + // A class with __eq__ and no __hash__ makes unhashable instances. + fn eq_without_hash(&self, _cls: Val) -> bool { false } + // Precomputes a user __hash__ which only the VM runs. + fn keyed(&mut self, _v: Val) -> Result<(), VmErr> { Ok(()) } + // KeyError whose text is the repr of the missing key. + fn key_error(&mut self, key: Val) -> VmErr { VmErr::Raised(s!("KeyError: ", str &self.repr(key))) } + // Repr of an exception instance which only the VM makes. + fn repr_other(&self, _v: Val, _seen: &mut Vec) -> Option { None } + + #[inline] fn truthy(&self, v: Val) -> bool { self.heap().truthy(v) } + #[inline] fn as_i128(&self, v: Val) -> Option { super::as_i128(v, self.heap()) } + // None for anything but a str. + #[inline] fn text(&self, v: Val) -> Option<&str> { match self.heap().try_get(v) { Some(HeapObj::Str(s)) => Some(s), _ => None } } + #[inline] fn alloc_and_push_str(&mut self, s: String) -> Result<(), VmErr> { let v = self.heap_mut().alloc(HeapObj::Str(s))?; self.push(v); Ok(()) } + #[inline] fn alloc_list(&mut self, items: Vec) -> Result { self.heap_mut().alloc(HeapObj::List(alloc::rc::Rc::new(core::cell::RefCell::new(items)))) } + #[inline] fn alloc_and_push_list(&mut self, items: Vec) -> Result<(), VmErr> { let v = self.alloc_list(items)?; self.push(v); Ok(()) } + #[inline] fn alloc_and_push_dict(&mut self, d: super::DictMap) -> Result<(), VmErr> { let v = self.heap_mut().alloc(HeapObj::Dict(alloc::rc::Rc::new(core::cell::RefCell::new(d))))?; self.push(v); Ok(()) } + + // TypeError for a non-iterable operand. + fn not_iterable(&self, o: Val) -> VmErr { + VmErr::TypeMsg(s!("'", str self.type_name(o), "' object is not iterable")) + } + + /* What a dict view shows now, an items view pairing each key with its value. */ + fn view_items(&mut self, d: Val, kind: View) -> Result, VmErr> { + let entries: Vec<(Val, Val)> = self.heap().dict_cell(d).map_or_else(Vec::new, |m| m.borrow().iter().collect()); + match kind { + View::Keys => Ok(entries.into_iter().map(|(k, _)| k).collect()), + View::Values => Ok(entries.into_iter().map(|(_, v)| v).collect()), + View::Items => entries.into_iter().map(|(k, v)| self.heap_mut().alloc(HeapObj::Tuple(vec![k, v]))).collect(), + } + } + + fn str_to_char_vals(&mut self, s: &str) -> Result, VmErr> { + // Per-char heap allocs scale with input, charge the budget so loops over this stay bounded. + self.charge(s.len())?; + s.chars().map(|c| self.heap_mut().alloc(HeapObj::Str(c.to_string()))).collect() + } + + // Items charged as native work and refused as iter refuses them. + fn extract_iter(&mut self, o: Val) -> Result, VmErr> { + if !o.is_heap() { + return Err(self.not_iterable(o)); + } + if let Some(items) = self.vm_items(o)? { return Ok(items); } + if let HeapObj::DictView(d, kind) = *self.heap().get(o) { + let items = self.view_items(d, kind)?; + self.charge(items.len())?; + return Ok(items); + } + // A wide range steps its cursor, the count it would hold checked against the memory left. + if let Some(&HeapObj::BigRange(cur, end, step)) = self.heap().try_get(o) { + let n = crate::frontend::int::range_len(self.heap(), o).unwrap_or(Ok(0))?; + if (n as u128).saturating_mul(VAL_BYTES as u128) > self.heap().room() as u128 { return Err(cold_heap()); } + let (mut walk, mut out) = (IterFrame::BigRange { cur, end, step }, Vec::new()); + while let Some(x) = walk.next_item(self.heap_mut())? { out.push(x); } + self.charge(out.len())?; + return Ok(out); + } + // Snapshot the variant out so the &self borrow ends before any allocation. + let snapshot = match self.heap().get(o) { + HeapObj::List(v) => Some(v.borrow().clone()), + HeapObj::Tuple(v) => Some(v.clone()), + HeapObj::Set(v) => Some(v.borrow().iter().cloned().collect()), + HeapObj::FrozenSet(v) => Some(v.iter().cloned().collect()), + HeapObj::Range(s, e, st) => { + let (mut cur, end, step) = (*s, *e, *st); + // Materialised length is user-controlled, cap it against the memory left. + let span = (end as i128 - cur as i128).unsigned_abs(); + let count = if step == 0 { 0 } else { span / (step as i128).unsigned_abs() }; + if count.saturating_mul(VAL_BYTES as u128) > self.heap().room() as u128 { return Err(cold_heap()); } + let mut out = Vec::new(); + if step > 0 { + while cur < end { + out.push(self.heap_mut().int(cur as i128)?); + match cur.checked_add(step) { Some(n) => cur = n, None => break } + } + } else { + while cur > end { + out.push(self.heap_mut().int(cur as i128)?); + match cur.checked_add(step) { Some(n) => cur = n, None => break } + } + } + Some(out) + } + HeapObj::Dict(d) | HeapObj::FrozenDict(d) => Some(d.borrow().keys().collect()), + HeapObj::Bytes(b) | HeapObj::ByteArray(b) => Some(b.iter().map(|&x| Val::int(x as i64)).collect()), + HeapObj::MemoryView(_) => Some(self.heap().octets(o).unwrap_or_default().iter().map(|&x| Val::int(x as i64)).collect()), + HeapObj::Str(_) => None, // handled below, needs heap allocation + _ => return Err(self.not_iterable(o)), + }; + if let Some(v) = snapshot { + // Cost scales with element count, charge it so repeated materialisation stays bounded. + self.charge(v.len())?; + return Ok(v); + } + // Str path materialises one-char heap strings via the existing helper. + if let HeapObj::Str(s) = self.heap().get(o) { + let s = s.clone(); + return self.str_to_char_vals(&s); + } + unreachable!() + } + + /* `str` of exception `name` from its args, a KeyError key shows as its repr. */ + fn exc_text(&self, name: &str, args: &[Val], seen: &mut Vec) -> String { + match args { + [] => String::new(), + // A group reads as its message and how many members it holds. + [msg, excs] if crate::vm::globals::is_group_type(name) => { + let n = match self.heap().try_get(*excs) { Some(HeapObj::List(l)) => l.borrow().len(), Some(HeapObj::Tuple(t)) => t.len(), _ => 0 }; + let tail = if n == 1 { " sub-exception)" } else { " sub-exceptions)" }; + s!(str &self.display_d(*msg, seen), " (", int n as i64, str tail) + } + [a] if name == "KeyError" => self.repr_d(*a, seen), + [a] => self.display_d(*a, seen), + [no, msg, rest @ ..] if rest.len() <= 1 && crate::vm::globals::matches_exc_class(name, "OSError") => { + let mut o = s!("[Errno ", str &self.display_d(*no, seen), "] ", str &self.display_d(*msg, seen)); + if let [f] = rest { o.push_str(": "); o.push_str(&self.repr_d(*f, seen)); } + o + } + _ => { let mut o = s!(cap: 32; "("); self.append_reprs(&mut o, args.iter(), seen); o.push(')'); o } + } + } + + /* `{a, b}` or `frozenset({a, b})` read in place, `...` for a cycle. */ + fn set_repr<'b>(&self, v: Val, frozen: bool, len: usize, items: impl Iterator, seen: &mut Vec) -> String { + if len == 0 { return (if frozen { "frozenset()" } else { "set()" }).into(); } + let mut out = String::from(if frozen { "frozenset({" } else { "{" }); + let id = v.as_heap(); + if seen.contains(&id) { out.push_str("..."); } else { seen.push(id); self.append_reprs(&mut out, items, seen); seen.pop(); } + out.push_str(if frozen { "})" } else { "}" }); + out + } + + fn append_reprs<'b>(&self, out: &mut String, it: impl Iterator, seen: &mut Vec) { + let mut first = true; + for v in it { + // Bound breadth, a wide structure re-referencing one big child would render without limit. + if out.len() > MAX_REPR_LEN { out.push_str(", ..."); break; } + if !first { out.push_str(", "); } + out.push_str(&self.repr_d(*v, seen)); + first = false; + } + } + + fn display(&self, v: Val) -> String { self.display_d(v, &mut Vec::new()) } + + /* The text of a FOREIGN value where no error can surface, its type when the kind refuses. */ + fn foreign_text(&self, v: Val) -> String { + match self.heap().foreign_repr(v) { Some(Ok(s)) => s, _ => s!("<", str self.type_name(v), " object", str &at(v), ">") } + } + + /* Whether builtin method `id` over `recv` is unbound, read off its type, an operator dunder keeping that type as `recv`. */ + #[inline] + fn method_unbound(&self, recv: Val, id: crate::vm::methods::BuiltinMethodId) -> bool { + recv.is_undef() || id.ty() == "op" && matches!(self.heap().try_get(recv), Some(HeapObj::Type(_))) + } + + /* The type an unbound builtin method belongs to. */ + fn method_owner(&self, recv: Val, id: crate::vm::methods::BuiltinMethodId) -> &str { + match self.heap().try_get(recv) { Some(HeapObj::Type(n)) if !recv.is_undef() => n, _ => id.ty() } + } + + /* Cycle-aware display, `seen` tracks containers on the current path, so self-referential structures emit "..." instead of recursing forever (and its length bounds raw nesting depth). */ + fn display_d(&self, v: Val, seen: &mut Vec) -> String { + if seen.len() > RENDER_DEPTH_MAX { return "...".into(); } + if v.is_int() { let mut b = itoa::Buffer::new(); return b.format(v.as_int()).into(); } + if v.is_float() { + // Single source of truth for float text (Python repr rules, scientific + .0). + return crate::util::fstr::format_f64(v.as_float()); + } + if v.is_true() { return "True".into(); } + if v.is_false() { return "False".into(); } + if v.is_none() { return "None".into(); } + if v.is_foreign() { return self.foreign_text(v); } + match self.heap().get(v) { + &HeapObj::Boxed(n) => self.display_d(n, seen), + HeapObj::Str(s) => s.clone(), + HeapObj::Bytes(b) => format_bytes(b), + HeapObj::ByteArray(b) => s!("bytearray(", str "e_bytes(b, true), ")"), + HeapObj::MemoryView(m) => s!(str if m.released { ""), + HeapObj::Type(name) => s!(""), + HeapObj::Slice(s,e,st) => s!("slice(", str &self.display_d(*s, seen), ", ", str &self.display_d(*e, seen), ", ", str &self.display_d(*st, seen), ")"), + HeapObj::Range(s,e,st) => if *st == 1 { s!("range(", int *s, ", ", int *e, ")") } else { s!("range(", int *s, ", ", int *e, ", ", int *st, ")") }, + HeapObj::BigRange(..) => crate::frontend::int::range_repr(self.heap(), v).and_then(Result::ok).unwrap_or_else(|| "range(...)".into()), + HeapObj::List(l) => { let id = v.as_heap(); if seen.contains(&id) { return "[...]".into(); } seen.push(id); let mut o = s!(cap: 32; "["); self.append_reprs(&mut o, l.borrow().iter(), seen); o.push(']'); seen.pop(); o }, + HeapObj::Tuple(t) => { let id = v.as_heap(); if seen.contains(&id) { return "(...)".into(); } seen.push(id); let o = if t.len() == 1 { s!("(", str &self.repr_d(t[0], seen), ",)") } else { let mut o = s!(cap: 32; "("); self.append_reprs(&mut o, t.iter(), seen); o.push(')'); o }; seen.pop(); o }, + HeapObj::Dict(d) => self.dict_repr(v, &d.borrow(), seen), + HeapObj::FrozenDict(d) if d.borrow().is_empty() => "frozendict()".into(), + HeapObj::FrozenDict(d) => s!("frozendict(", str &self.dict_repr(v, &d.borrow(), seen), ")"), + // `object` spells its slot wrappers without the type. + &HeapObj::BoundMethod(recv, id) if self.method_unbound(recv, id) && is_dunder(id.name()) && id.ty() == "object" => s!(""), + &HeapObj::BoundMethod(recv, id) if self.method_unbound(recv, id) && is_dunder(id.name()) => s!(""), + &HeapObj::BoundMethod(recv, id) if self.method_unbound(recv, id) => s!(""), + &HeapObj::BoundMethod(recv, id) if is_dunder(id.name()) => s!(""), + &HeapObj::BoundMethod(recv, id) => s!(""), + HeapObj::NativeFn(id) => s!(""), + &HeapObj::Super(cls, recv) => { + let short = |c: Val| match self.heap().try_get(c) { Some(HeapObj::Class(n, ..) | HeapObj::Type(n)) => n.as_str(), _ => "?" }; + // A class bound reads as an object of that class, as an instance reads by its type. + if recv.is_none() { return s!(", NULL>"); } + let of = if matches!(self.heap().try_get(recv), Some(HeapObj::Class(..))) { short(recv) } else { self.type_name(recv) }; + s!(", <", str of, " object>>") + } + HeapObj::Property(..) => s!(""), + HeapObj::PropertyPart(_, slot) => crate::s!(""), + &HeapObj::StaticMethod(f) => s!(""), + &HeapObj::ClassMethod(f) => s!(""), + HeapObj::Iter(_, name) => s!("<", str name, " object", str &at(v), ">"), + HeapObj::DictView(d, kind) => { + let mut o = s!(str self.type_name(v), "(["); + if let Some(m) = self.heap().dict_cell(*d) { + for (i, (k, val)) in m.borrow().iter().enumerate() { + if i > 0 { if o.len() > MAX_REPR_LEN { o.push_str(", ..."); break; } o.push_str(", "); } + match kind { + View::Keys => o.push_str(&self.repr_d(k, seen)), + View::Values => o.push_str(&self.repr_d(val, seen)), + View::Items => { o.push('('); o.push_str(&self.repr_d(k, seen)); o.push_str(", "); o.push_str(&self.repr_d(val, seen)); o.push(')'); } + } + } + } + o.push_str("])"); + o + } + HeapObj::Module(name, _) => s!(""), + HeapObj::Extern(f) => s!(""), + HeapObj::TypeAlias(name, _) => name.clone(), + HeapObj::TypeVar(name) => name.trim_start_matches('*').into(), + HeapObj::ExcInstance(name, args, _) => self.exc_text(name, args, seen), + HeapObj::Set(s) => { let s = s.borrow(); self.set_repr(v, false, s.len(), s.iter(), seen) } + HeapObj::FrozenSet(s) => self.set_repr(v, true, s.len(), s.iter(), seen), + HeapObj::Ellipsis => "Ellipsis".into(), + HeapObj::NotImplemented => "NotImplemented".into(), + // A cell names what it holds by type, an unfilled one is empty. + &HeapObj::Cell(c) if c.is_undef() => s!(""), + &HeapObj::Cell(c) => s!(""), + HeapObj::Code(c) => s!("", str &at(v), ", file \"", str &c.filename, "\", line 1>"), + + _ => self.display_other(v, seen), + } + } + + /* `{k: v, ...}` of the entries of dict `v`, `{...}` where it holds itself. */ + fn dict_repr(&self, v: Val, d: &DictMap, seen: &mut Vec) -> String { + let id = v.as_heap(); + if seen.contains(&id) { return "{...}".into(); } + seen.push(id); + let mut o = s!(cap: 32; "{"); + for (i, (k, val)) in d.iter().enumerate() { + if i > 0 { + if o.len() > MAX_REPR_LEN { o.push_str(", ..."); break; } + o.push_str(", "); + } + o.push_str(&self.repr_d(k, seen)); + o.push_str(": "); + o.push_str(&self.repr_d(val, seen)); + } + o.push('}'); + seen.pop(); + o + } + + fn repr(&self, v: Val) -> String { self.repr_d(v, &mut Vec::new()) } + + fn repr_d(&self, v: Val, seen: &mut Vec) -> String { + if v.is_heap() { + match self.heap().get(v) { + HeapObj::Str(s) => return repr_str(s), + // `repr(E("x"))` is the constructor call, `ValueError('x')`. + HeapObj::ExcInstance(name, args, _) => { + let mut o = s!(cap: 32; str name, "("); + self.append_reprs(&mut o, args.iter(), seen); + o.push(')'); + return o; + } + _ => if let Some(o) = self.repr_other(v, seen) { return o; }, + } + } + self.display_d(v, seen) + } + + /* Reject mutable types (list/dict/set) used as dict/set keys, plus instances whose class defines `__eq__`. */ + fn require_hashable(&self, v: Val) -> Result<(), VmErr> { self.require_hashable_at(v, 0) } + + /* A set probe of a set looks up as the frozenset it equals, so only other values must hash. */ + fn require_set_probe(&self, v: Val) -> Result<(), VmErr> { + if v.is_heap() && matches!(self.heap().get(v), HeapObj::Set(_)) { return Ok(()); } + self.require_element(v) + } + + /* `v` as a dict key, an unhashable one named for that use as well as for its type. */ + fn require_key(&self, v: Val) -> Result<(), VmErr> { self.require_hashable(v).map_err(|e| self.unhashable_as(v, &e, "a dict key")) } + + /* `v` as a set element, an unhashable one named for that use as well as for its type. */ + fn require_element(&self, v: Val) -> Result<(), VmErr> { self.require_hashable(v).map_err(|e| self.unhashable_as(v, &e, "a set element")) } + + #[cold] + fn unhashable_as(&self, v: Val, e: &VmErr, what: &str) -> VmErr { + VmErr::TypeMsg(crate::s!("cannot use '", str self.type_name(v), "' as ", str what, " (", str &e.message(), ")")) + } + + fn require_hashable_at(&self, v: Val, depth: usize) -> Result<(), VmErr> { + if v.is_heap() && depth <= EQ_DEPTH_MAX { + match self.heap().get(v) { + HeapObj::List(_) => return Err(cold_type("unhashable type: 'list'")), + HeapObj::ByteArray(_) => return Err(cold_type("unhashable type: 'bytearray'")), + HeapObj::MemoryView(m) if m.released => return Err(cold_value("operation forbidden on released memoryview object")), + HeapObj::MemoryView(m) if matches!(self.heap().try_get(m.obj), Some(HeapObj::ByteArray(_))) => return Err(cold_value("cannot hash writable memoryview object")), + HeapObj::Dict(_) => return Err(cold_type("unhashable type: 'dict'")), + HeapObj::Set(_) => return Err(cold_type("unhashable type: 'set'")), + HeapObj::DictView(..) => return Err(VmErr::TypeMsg(crate::s!("unhashable type: '", str self.type_name(v), "'"))), + // A tuple hashes through its items as deep as the hash looks, so each must be hashable too. + HeapObj::Tuple(items) => for &item in items { self.require_hashable_at(item, depth + 1)?; }, + // A frozendict hashes through its values too, its keys hashable already. + HeapObj::FrozenDict(d) => for (_, item) in d.borrow().iter() { self.require_hashable_at(item, depth + 1)?; }, + // A `__eq__` without `__hash__` would let two equal keys hash apart. + HeapObj::Instance(cls, _) if self.eq_without_hash(*cls) => return Err(VmErr::TypeMsg(crate::s!("unhashable type: '", str self.type_name(v), "'"))), + _ => {} + } + } + Ok(()) + } + + /* `v` as one byte, the int `bytearray` items must be. */ + fn byte_value(&self, v: Val) -> Result { + let v = crate::value::unboxed(v, self.heap()); + let Some(n) = self.as_i128(v).filter(|_| !v.is_float()) else { + return Err(VmErr::TypeMsg(crate::s!("'", str self.type_name(v), "' object cannot be interpreted as an integer"))); + }; + u8::try_from(n).map_err(|_| cold_value("byte must be in range(0, 256)")) + } + + /* KeyError carrying the text `msg`, as the methods that find nothing to remove raise. */ + fn text_key_error(&mut self, msg: &str) -> VmErr { + match self.heap_mut().alloc(HeapObj::Str(msg.into())) { Ok(k) => self.key_error(k), Err(e) => e } + } + + /* A set of `items`, each one hashable. */ + fn valset_of(&mut self, items: &[Val]) -> Result { + for &v in items.iter().filter(|v| v.is_heap()) { self.require_element(v)?; self.keyed(v)?; } + Ok(ValSet::from_vals(items, self.heap())) + } + + /* A dict of `pairs` in order, a later equal key overwriting the value and keeping the first key. */ + fn dictmap_of(&mut self, pairs: Vec<(Val, Val)>) -> Result { + for &(k, _) in pairs.iter().filter(|p| p.0.is_heap()) { self.require_key(k)?; self.keyed(k)?; } + Ok(DictMap::from_pairs(pairs, self.heap())) + } + + /* Alloc a set-algebra result, frozen picks frozenset (left-operand type rule). */ + fn alloc_set_result(&mut self, items: Vec, frozen: bool) -> Result { + let s = ValSet::from_vals(&items, self.heap()); + if frozen { self.heap_mut().alloc(HeapObj::FrozenSet(Rc::new(s))) } + else { self.heap_mut().alloc(HeapObj::Set(Rc::new(RefCell::new(s)))) } + } + + /* Wrap an i128 into the narrowest Val, None->Overflow, 48-bit->inline, else boxed. */ + #[inline] + fn int_to_val(&mut self, r: Option) -> Result { + self.heap_mut().int(r.ok_or_else(cold_overflow)?) + } + + // Allocate a Set from `items` (deduped by content) and push. Mirrors `alloc_and_push_list`. + fn alloc_and_push_set(&mut self, items: Vec) -> Result<(), VmErr> { + let v = self.alloc_set_result(items, false)?; + self.push(v); Ok(()) + } + + /* The view `v` holds, refused once released. */ + fn view(&self, v: Val) -> Option> { + match self.heap().try_get(v)? { + HeapObj::MemoryView(m) if m.released => Some(Err(released())), + &HeapObj::MemoryView(m) => Some(Ok(m)), + _ => None, + } + } + + /* `m[i]` of a view, the byte as an int. */ + fn view_item(&self, m: MemView, i: Val) -> Result { + let Some(i) = self.as_i128(i).filter(|_| !i.is_float()) else { + return Err(VmErr::TypeMsg(s!("memoryview: invalid slice key"))); + }; + let at = crate::vm::memview::at(&m, i)?; + let byte = self.heap().bytes_of(m.obj).and_then(|b| b.get(at)).copied().ok_or_else(|| cold_index("index out of bounds on dimension 1"))?; + Ok(Val::int(byte as i64)) + } + + /* `m[a:b:c]`, a view of part of the view. */ + fn view_slice(&mut self, m: MemView, start: Val, stop: Val, step: Val) -> Result { + let clip = |v: Val| crate::frontend::int::clip_index(self.heap(), v); + let (s, e, st) = slice_bounds(clip(start), clip(stop), clip(step), m.len as i64)?; + let len = if st > 0 { ((e - s).max(0) + st - 1) / st } else { ((s - e).max(0) - st - 1) / -st } as usize; + let start = (m.start as isize + s as isize * m.step).max(0) as usize; + self.heap_mut().alloc(HeapObj::MemoryView(MemView { obj: m.obj, start, len, step: m.step * st as isize, released: false })) + } + + /* No-dunder indexing path. Used by callers without a bytecode frame (FFI re-entry) and as the post-dunder fallback inside `get_item`. */ + fn get_item_builtin(&mut self, obj: Val, idx: Val) -> Result<(), VmErr> { + if idx.is_heap() + && let &HeapObj::Slice(start, stop, step) = self.heap().get(idx) { + let v = self.slice_val(obj, start, stop, step)?; + self.push(v); + return Ok(()); + } + + let ascii = idx.is_int() && self.heap_mut().str_is_ascii(obj); + if obj.is_heap() && idx.is_int() + && let HeapObj::Str(s) = self.heap().get(obj) { + let i = idx.as_int(); + let one: String = if ascii { + let b = s.as_bytes(); + let c = *b.get(normalize_index(i, b.len())).ok_or_else(|| cold_index("string index out of range"))?; + (c as char).to_string() + } else { + let ui = normalize_index(i, s.chars().count()); + s.chars().nth(ui).ok_or_else(|| cold_index("string index out of range"))?.to_string() + }; + let val = self.heap_mut().alloc(HeapObj::Str(one))?; + self.push(val); + return Ok(()); + } + + // `bytes[i]` returns the byte as int (`0..=255`), unlike `str[i]` (length-1 str). + if obj.is_heap() && idx.is_int() + && let HeapObj::Bytes(b) | HeapObj::ByteArray(b) = self.heap().get(obj) { + let i = idx.as_int(); + let ui = normalize_index(i, b.len()); + let what = if matches!(self.heap().get(obj), HeapObj::ByteArray(_)) { "bytearray index out of range" } else { "index out of range" }; + let byte = *b.get(ui).ok_or_else(|| cold_index(what))?; + self.push(Val::int(byte as i64)); + return Ok(()); + } + + let v = self.getitem_val(obj, idx)?; + self.push(v); + Ok(()) + } + + fn slice_val(&mut self, obj: Val, start: Val, stop: Val, step: Val) -> Result { + if !obj.is_heap() { return Err(cold_type("slice requires a sequence")); } + if let Some(m) = self.view(obj) { return self.view_slice(m?, start, stop, step); } + + // Item count without materialising the source. + let ascii = self.heap_mut().str_is_ascii(obj); + let len: i64 = match self.heap().get(obj) { + HeapObj::List(v) => v.borrow().len() as i64, + HeapObj::Tuple(v) => v.len() as i64, + HeapObj::Str(s) => if ascii { s.len() as i64 } else { s.chars().count() as i64 }, + HeapObj::Bytes(b) | HeapObj::ByteArray(b) => b.len() as i64, + // A range slices into another range, its bounds resolved at any width. + HeapObj::Range(..) | HeapObj::BigRange(..) => return crate::frontend::int::range_slice(self.heap_mut(), obj, start, stop, step).unwrap_or(Err(VmErr::Overflow)), + _ => return Err(cold_type("object is not sliceable")), + }; + // A boxed index lies past either end of any sequence, so it clamps like the inline extreme. + let clip = |v: Val| crate::frontend::int::clip_index(self.heap(), v); + let (s, e, st) = slice_bounds(clip(start), clip(stop), clip(step), len)?; + + // Step 1 copies one contiguous range. + let contiguous = st == 1; + let (lo, hi) = (s.max(0) as usize, e.max(s).max(0) as usize); + let mut indices = Vec::new(); + if !contiguous { + let mut cur = s; + if st > 0 { while cur < e { indices.push(cur as usize); cur += st; } } + else { while cur > e { indices.push(cur as usize); cur += st; } } + } + let pick = |v: &[Val]| -> Vec { + if contiguous { return v[lo.min(v.len())..hi.min(v.len())].to_vec(); } + indices.iter().filter_map(|&i| v.get(i).copied()).collect() + }; + let pick_bytes = |b: &[u8]| -> Vec { + if contiguous { return b[lo.min(b.len())..hi.min(b.len())].to_vec(); } + indices.iter().filter_map(|&i| b.get(i).copied()).collect() + }; + + // Pick while borrowing the source, allocate after. + enum Out { List(Vec), Tuple(Vec), Str(String), Bytes(Vec), ByteArray(Vec) } + let out = match self.heap().get(obj) { + HeapObj::List(v) => Out::List(pick(&v.borrow())), + HeapObj::Tuple(v) => Out::Tuple(pick(v)), + HeapObj::Str(text) if ascii => Out::Str(String::from_utf8(pick_bytes(text.as_bytes())).unwrap_or_default()), + HeapObj::Str(text) => { + let chars: Vec = text.chars().collect(); + Out::Str(if contiguous { chars[lo.min(chars.len())..hi.min(chars.len())].iter().collect() } + else { indices.iter().filter_map(|&i| chars.get(i)).collect() }) + } + HeapObj::Bytes(buf) => Out::Bytes(pick_bytes(buf)), + HeapObj::ByteArray(buf) => Out::ByteArray(pick_bytes(buf)), + _ => return Err(cold_type("object is not sliceable")), + }; + match out { + Out::List(v) => self.heap_mut().alloc(HeapObj::List(Rc::new(RefCell::new(v)))), + Out::Tuple(v) => self.heap_mut().alloc(HeapObj::Tuple(v)), + Out::Str(s) => self.heap_mut().alloc(HeapObj::Str(s)), + Out::Bytes(b) => self.heap_mut().alloc(HeapObj::Bytes(b)), + Out::ByteArray(b) => self.heap_mut().alloc(HeapObj::ByteArray(b)), + } + } + + fn getitem_val(&mut self, obj: Val, idx: Val) -> Result { + if !obj.is_heap() { return Err(VmErr::TypeMsg(crate::s!("'", str self.type_name(obj), "' object is not subscriptable"))); } + let bad = |vm: &Self, kind: &str| if crate::frontend::int::is_big(vm.heap(), idx) { cold_index("cannot fit 'int' into an index-sized integer") } + // A string words it without slices and quotes the type. + else if kind == "string" { VmErr::TypeMsg(crate::s!("string indices must be integers, not '", str vm.type_name(idx), "'")) } + else { VmErr::TypeMsg(crate::s!(str kind, " indices must be integers or slices, not ", str vm.type_name(idx))) }; + if self.heap().dict_cell(obj).is_some() { self.keyed(idx)?; } + match self.heap().get(obj) { + HeapObj::List(v) => { + if !idx.is_int() { return Err(bad(self, "list")); } + let b = v.borrow(); let i = idx.as_int(); + let ui = normalize_index(i, b.len()); + b.get(ui).copied().ok_or_else(|| cold_index("list index out of range")) + } + HeapObj::Tuple(v) => { + if !idx.is_int() { return Err(bad(self, "tuple")); } + let i = idx.as_int(); + let ui = normalize_index(i, v.len()); + v.get(ui).copied().ok_or_else(|| cold_index("tuple index out of range")) + } + HeapObj::Dict(p) | HeapObj::FrozenDict(p) => { + let hit = p.borrow().get(&idx, self.heap()).copied(); + match hit { + Some(v) => Ok(v), + None => { self.require_key(idx)?; Err(self.key_error(idx)) } + } + } + HeapObj::Str(_) => Err(bad(self, "string")), + HeapObj::Bytes(_) => Err(bad(self, "byte")), + HeapObj::ByteArray(_) => Err(bad(self, "bytearray")), + HeapObj::MemoryView(_) => { let m = self.view(obj).unwrap_or(Err(cold_heap()))?; self.view_item(m, idx) } + &HeapObj::BigRange(..) if idx.is_int() || crate::frontend::int::is_big(self.heap(), idx) => crate::frontend::int::range_item(self.heap_mut(), obj, idx).unwrap_or(Err(VmErr::Overflow)), + HeapObj::BigRange(..) => Err(bad(self, "range")), + // `range(n)[i]` is computed, never materialised. + &HeapObj::Range(s, e, st) => { + // An index past i64 lies outside any range. + if crate::frontend::int::is_big(self.heap(), idx) { return Err(cold_index("range object index out of range")); } + if !idx.is_int() { return Err(bad(self, "range")); } + let (i, len) = (idx.as_int(), range_len(s, e, st) as i64); + let i = if i < 0 { i + len } else { i }; + if !(0..len).contains(&i) { return Err(cold_index("range object index out of range")); } + self.heap_mut().int((s + i * st) as i128) + } + // `list[int]` and `Pair[int]` build a generic alias, the args kept as one tuple. + HeapObj::Type(n) if matches!(n.as_str(), "list" | "tuple" | "dict" | "set" | "frozenset" | "type") => self.generic_alias(obj, idx), + HeapObj::TypeAlias(..) => self.generic_alias(obj, idx), + _ => Err(VmErr::TypeMsg(crate::s!("'", str self.type_name(obj), "' object is not subscriptable"))), + } + } + + fn generic_alias(&mut self, origin: Val, idx: Val) -> Result { + let args = if idx.is_heap() && matches!(self.heap().get(idx), HeapObj::Tuple(_)) { idx } else { self.heap_mut().alloc(HeapObj::Tuple(alloc::vec![idx]))? }; + self.heap_mut().alloc(HeapObj::GenericAlias(origin, args)) + } + + /* `len(o)` of a builtin container, shared with the plugin ABI. */ + fn builtin_len(&mut self, o: Val) -> Result { + let ascii = self.heap_mut().str_is_ascii(o); + Ok(match self.heap().try_get(o) { + Some(HeapObj::Str(s)) => (if ascii { s.len() } else { s.chars().count() }) as i128, + Some(HeapObj::Bytes(b) | HeapObj::ByteArray(b)) => b.len() as i128, + Some(HeapObj::MemoryView(m)) => if m.released { return Err(cold_value("operation forbidden on released memoryview object")) } else { m.len as i128 }, + Some(HeapObj::List(v)) => v.borrow().len() as i128, + Some(HeapObj::Tuple(v)) => v.len() as i128, + Some(HeapObj::Dict(v) | HeapObj::FrozenDict(v)) => v.borrow().len() as i128, + Some(&HeapObj::DictView(d, _)) => return self.builtin_len(d), + Some(HeapObj::Set(v)) => v.borrow().len() as i128, + Some(HeapObj::FrozenSet(v)) => v.len() as i128, + Some(&HeapObj::Range(_, _, 0)) => return Err(cold_value("range() arg 3 must not be zero")), + Some(&HeapObj::Range(s, e, st)) => crate::value::eq::range_len(s, e, st), + Some(HeapObj::BigRange(..)) => crate::frontend::int::range_len(self.heap(), o).unwrap_or(Ok(0))?, + _ => return Err(VmErr::TypeMsg(s!("object of type '", str self.type_name(o), "' has no len()"))), + }) + } + + /* Item presence in list/tuple/dict/set, or substring in string. Non-iterable container raises TypeError. */ + fn contains(&mut self, container: Val, item: Val) -> Result { + if container.is_heap() && matches!(self.heap().get(container), HeapObj::Dict(_) | HeapObj::FrozenDict(_) | HeapObj::Set(_) | HeapObj::FrozenSet(_)) { self.keyed(item)?; } + if container.is_heap() { + match self.heap().get(container) { + // A scan counts what it may read as native work. + HeapObj::List(v) => { self.heap().spend(v.borrow().len()); return Ok(v.borrow().iter().any(|x| eq_member(*x, item, self.heap()))) } + HeapObj::Tuple(v) => { self.heap().spend(v.len()); return Ok(v.iter().any(|x| eq_member(*x, item, self.heap()))) } + // Only a miss checks the item, an unhashable one is never inside so a hit needs no check. + HeapObj::Dict(p) | HeapObj::FrozenDict(p) => { let hit = p.borrow().contains_key(&item, self.heap()); if !hit { self.require_key(item)?; } return Ok(hit); } + HeapObj::Set(s) => { let hit = s.borrow().contains(item, self.heap()); if !hit { self.require_set_probe(item)?; } return Ok(hit); } + HeapObj::FrozenSet(s) => { let hit = s.contains(item, self.heap()); if !hit { self.require_set_probe(item)?; } return Ok(hit); } + HeapObj::Str(s) => { + if let Some(HeapObj::Str(sub)) = self.heap().try_get(item) { self.heap().spend(s.len()); return Ok(s.contains(sub.as_str())); } + return Err(VmErr::TypeMsg(s!("'in ' requires string as left operand, not ", str self.type_name(item)))); + } + HeapObj::Bytes(b) | HeapObj::ByteArray(b) => { + if let Some(sub) = self.heap().bytes_of(item) { + self.heap().spend(b.len()); + return Ok(sub.is_empty() || b.windows(sub.len()).any(|w| w == sub)); + } + let n = match item { i if i.is_int() => i.as_int(), i if i.is_bool() => i.as_bool() as i64, _ => return Err(cold_type("a bytes-like object is required")) }; + if !(0..=255).contains(&n) { return Err(cold_value("byte must be in range(0, 256)")); } + return Ok(b.contains(&(n as u8))); + } + HeapObj::BigRange(..) => return Ok(crate::frontend::int::range_has(self.heap(), container, item).unwrap_or(false)), + HeapObj::Range(s, e, st) => { + let (s, e, st) = (*s as i128, *e as i128, *st as i128); + // O(1) range membership via bounds + step, integral floats match too. + let x = match as_i128(item, self.heap()) { + Some(i) => Some(i), + None if item.is_float() => { + let v = item.as_float(); + if v.is_finite() && v == libm::trunc(v) { Some(v as i128) } else { None } + } + None => None, + }; + return Ok(match x { + Some(x) => { + let in_bounds = if st > 0 { x >= s && x < e } else { x <= s && x > e }; + in_bounds && st != 0 && (x - s) % st == 0 + } + None => false, + }); + } + _ => {} + } + } + Err(VmErr::TypeMsg(s!("argument of type '", str self.type_name(container), "' is not a container or iterable"))) + } + + // None for an instance or a slice that may hold one. + #[inline] + fn plain_index(&self, cont: Val, idx: Val) -> Option { + // `xs[True]` indexes like `xs[1]`, a dict keeps the bool key. + let keyed = self.heap().dict_cell(cont).is_some(); + if idx.is_bool() && !keyed { return Some(Val::int(idx.as_bool() as i64)); } + if !idx.is_heap() || keyed { return Some(idx); } + // An int of a class deriving from int indexes as the int. + if let &HeapObj::Boxed(n) = self.heap().get(idx) && (n.is_int() || n.is_foreign()) { return Some(n); } + None + } + + /* `C.__float__() must return a float, not int`, what a conversion dunder of `obj` that returned `got` raises. */ + #[cold] + #[inline(never)] + fn returned_wrong(&self, obj: Val, dunder: &str, want: &str, got: Val) -> VmErr { + VmErr::TypeMsg(crate::s!(str self.type_name(obj), ".", str dunder, "() must return ", str want, ", not ", str self.type_name(got))) + } + + /* `bad operand type for : ''`, what an operator or builtin of one operand says of a value it cannot take. */ + #[cold] + #[inline(never)] + fn bad_operand(&self, what: &str, v: Val) -> VmErr { VmErr::TypeMsg(crate::s!("bad operand type for ", str what, ": '", str self.type_name(v), "'")) } + + /* Whether `v` is an instance of a user class. */ + #[inline(always)] + fn is_instance(&self, v: Val) -> bool { v.is_heap() && matches!(self.heap().get(v), HeapObj::Instance(..)) } + + /* Class of an Instance or of a dict or list a user class made, `None` for built-in operands. */ + #[inline] + fn instance_class(&self, v: Val) -> Option { crate::vm::opcodes::dunder::class_of(self.heap(), v) } + + /* A builtin iterator named `name` over `frame`. */ + fn alloc_iterator(&mut self, frame: IterFrame, name: &'static str) -> Result { + self.heap_mut().alloc(HeapObj::Iter(alloc::rc::Rc::new(core::cell::RefCell::new(frame)), name)) + } + + /* Pushes a builtin iterator named `name` over `frame`. */ + fn push_iterator(&mut self, frame: IterFrame, name: &'static str) -> Result<(), VmErr> { + let it = self.alloc_iterator(frame, name)?; + self.push(it); + Ok(()) + } + + /* A live walk over dict `d` showing the part `kind` names. */ + fn dict_walk(&self, d: Val, kind: View) -> IterFrame { + let len = self.heap().dict_cell(d).map_or(0, |m| m.borrow().len()); + IterFrame::DictWalk { of: d, idx: 0, left: len, len, kind } + } + + /* A frame over `items` read from `o`, watching a dict, dict view or set for a change of size. */ + fn watched(&self, o: Val, items: Vec) -> IterFrame { + let of = match self.heap().try_get(o) { + Some(HeapObj::Dict(_) | HeapObj::FrozenDict(_) | HeapObj::Set(_)) => o, + Some(&HeapObj::DictView(d, _)) => d, + _ => return IterFrame::Seq { items: items.into(), idx: 0 }, + }; + IterFrame::Watched { items: items.into(), idx: 0, of, len: crate::value::watched_len(self.heap(), of) } + } + + // Interned keyword name behind a Val, or None when not a string. + fn kw_name(&self, k: Val) -> Option<&str> { + match self.heap().try_get(k) { + Some(HeapObj::Str(s)) => Some(s.as_str()), + _ => None, + } + } + + fn add_vals(&mut self, a: Val, b: Val) -> Result { + // Inline-int fast path, overflow falls through to the i128 slow path. + if a.is_int() && b.is_int() + && let Some(r) = a.as_int().checked_add(b.as_int()) + && (Val::INT_MIN..=Val::INT_MAX).contains(&r) { + return Ok(Val::int(r)); + } + if let Some(p) = coerce_floats(a, b, self.heap()) { let (af, bf) = p?; return Ok(Val::float(af + bf)); } + if let (Some(ai), Some(bi)) = (as_i128(a, self.heap()), as_i128(b, self.heap())) && let Some(r) = ai.checked_add(bi) { return self.heap_mut().int(r); } + if let Some(r) = self.wide_op(BinOp::Add, a, b)? { return Ok(r); } + if a.is_heap() && b.is_heap() { + // Charge the copy cost so growing concatenation in a loop stays bounded (avoids O(n^2)). + let copy_cost = match (self.heap().get(a), self.heap().get(b)) { + (HeapObj::Str(sa), HeapObj::Str(sb)) => Some(sa.len() + sb.len()), + (HeapObj::Bytes(ba) | HeapObj::ByteArray(ba), HeapObj::Bytes(bb) | HeapObj::ByteArray(bb)) => Some(ba.len() + bb.len()), + (HeapObj::List(va), HeapObj::List(vb)) => Some(va.borrow().len() + vb.borrow().len()), + (HeapObj::Tuple(va), HeapObj::Tuple(vb)) => Some(va.len() + vb.len()), + _ => None, + }; + if let Some(n) = copy_cost { self.charge(n)?; } + match (self.heap().get(a), self.heap().get(b)) { + (HeapObj::Str(sa), HeapObj::Str(sb)) => { + let mut r = String::with_capacity(sa.len() + sb.len()); + r.push_str(sa); r.push_str(sb); + return self.heap_mut().alloc(HeapObj::Str(r)); + } + // Each result is sized once, so no operand is copied twice. + (HeapObj::List(va), HeapObj::List(vb)) => { + let lst = [&va.borrow()[..], &vb.borrow()[..]].concat(); + return self.heap_mut().alloc(HeapObj::List(Rc::new(RefCell::new(lst)))); + } + (HeapObj::Tuple(va), HeapObj::Tuple(vb)) => { + let tup = [&va[..], &vb[..]].concat(); + return self.heap_mut().alloc(HeapObj::Tuple(tup)); + } + // The result takes the type of the left side. + (left @ (HeapObj::Bytes(ba) | HeapObj::ByteArray(ba)), HeapObj::Bytes(bb) | HeapObj::ByteArray(bb)) => { + let (r, array) = ([&ba[..], &bb[..]].concat(), matches!(left, HeapObj::ByteArray(_))); + return self.heap_mut().alloc(if array { HeapObj::ByteArray(r) } else { HeapObj::Bytes(r) }); + } + _ => {} + } + } + Err(self.concat_err(a, b).unwrap_or_else(|| self.unsupported("+", a, b))) + } + + /* The error a sequence on the left of `+` raises for a right side of another type. */ + #[cold] + fn concat_err(&self, a: Val, b: Val) -> Option { + let (left, right) = (self.type_name(a), self.type_name(b)); + match self.heap().try_get(a)? { + HeapObj::Str(_) | HeapObj::List(_) | HeapObj::Tuple(_) => Some(VmErr::TypeMsg(s!("can only concatenate ", str left, " (not \"", str right, "\") to ", str left))), + HeapObj::Bytes(_) | HeapObj::ByteArray(_) => Some(VmErr::TypeMsg(s!("can't concat ", str right, " to ", str left))), + _ => None, + } + } + + /* TypeError for a binary operator no operand type supports. */ + #[cold] + fn unsupported(&self, op: &str, a: Val, b: Val) -> VmErr { + VmErr::TypeMsg(s!("unsupported operand type(s) for ", str op, ": '", str self.type_name(a), "' and '", str self.type_name(b), "'")) + } + + /* `op` through the kind of a FOREIGN operand, None when nothing takes it. */ + fn wide_op(&mut self, op: BinOp, a: Val, b: Val) -> Result, VmErr> { + if !a.is_foreign() && !b.is_foreign() { return Ok(None); } + self.heap_mut().foreign_binop(op, a, b) + } + + fn lt_vals(&self, a: Val, b: Val) -> Result { + self.lt_vals_d(a, b, 0) + } + + /* Depth-tracked `<`, past the cap surface RecursionError instead of overflowing the stack. */ + fn lt_vals_d(&self, a: Val, b: Val, depth: usize) -> Result { + if depth > CMP_DEPTH_MAX { return Err(cold_depth()); } + let a = if a.is_bool() { Val::int(a.as_bool() as i64) } else { a }; + let b = if b.is_bool() { Val::int(b.as_bool() as i64) } else { b }; + if a.is_int() && b.is_int() { return Ok(a.as_int() < b.as_int()); } + if (a.is_foreign() || b.is_foreign()) && let Some(o) = self.heap().foreign_cmp(a, b) { return Ok(o == Ordering::Less); } + if let Some(Ok((af, bf))) = coerce_floats(a, b, self.heap()) { return Ok(af < bf); } + if a.is_heap() && b.is_heap() { + match (self.heap().get(a), self.heap().get(b)) { + (HeapObj::Str(x), HeapObj::Str(y)) => return Ok(x < y), + (HeapObj::Bytes(x) | HeapObj::ByteArray(x), HeapObj::Bytes(y) | HeapObj::ByteArray(y)) => return Ok(x < y), + // Sequences compare lexicographically, clone to drop the heap borrow before recursing. + (HeapObj::List(x), HeapObj::List(y)) => { + let (x, y) = (x.borrow().clone(), y.borrow().clone()); + return self.seq_lt_d(&x, &y, depth + 1); + } + (HeapObj::Tuple(x), HeapObj::Tuple(y)) => { + let (x, y) = (x.clone(), y.clone()); + return self.seq_lt_d(&x, &y, depth + 1); + } + _ => {} + } + } + // A number of a class over int or float orders as the number. + let (x, y) = (unboxed(a, self.heap()), unboxed(b, self.heap())); + if x.0 != a.0 || y.0 != b.0 { return self.lt_vals_d(x, y, depth); } + Err(VmErr::TypeMsg(s!("'<' not supported between instances of '", str self.type_name(a), "' and '", str self.type_name(b), "'"))) + } + + /* Lexicographic `<` for sequences, first differing element decides, otherwise the shorter is less. Recurses through `lt_vals`, so nested sequences and mixed element types are handled (and rejected) consistently. */ + fn seq_lt_d(&self, xs: &[Val], ys: &[Val], depth: usize) -> Result { + if depth > CMP_DEPTH_MAX { return Err(cold_depth()); } + for (&x, &y) in xs.iter().zip(ys.iter()) { + if eq_member(x, y, self.heap()) { continue; } + return self.lt_vals_d(x, y, depth + 1); + } + Ok(xs.len() < ys.len()) + } + + /* `a // b` or `a % b`, floats when either side is one, signed like the divisor. */ + fn floor_op(&mut self, op: BinOp, a: Val, b: Val, sym: &str) -> Result { + let modulo = op == BinOp::Mod; + if a.is_float() || b.is_float() { + let (Some(af), Some(bf)) = (num_f64(a, self.heap()), num_f64(b, self.heap())) else { return Err(self.unsupported(sym, a, b)); }; + let (af, bf) = (af?, bf?); + if bf == 0.0 { return Err(VmErr::ZeroDiv); } + let (q, r) = float_divmod(af, bf); + return Ok(Val::float(if modulo { r } else { q })); + } + if let (Some(x), Some(y)) = (self.as_i128(a), self.as_i128(b)) && y != 0 && let Some((q, r)) = int_divmod(x, y) { + return self.heap_mut().int(if modulo { r } else { q }); + } + self.heap_mut().foreign_binop(op, a, b)?.ok_or_else(|| self.unsupported(sym, a, b)) + } + + /* Two-arg power for `pow()` and `**`, exact for ints and a float for floats or negative exponents. */ + fn pow_vals(&mut self, a: Val, b: Val, err_msg: &'static str) -> Result { + if let (Some(ai), Some(exp)) = (self.as_i128(a), self.as_i128(b).and_then(|e| i64::try_from(e).ok())) { + if exp >= 0 { + if let Some(r) = checked_pow(ai, exp as u64) { return self.heap_mut().int(r); } + return self.heap_mut().foreign_binop(BinOp::Pow, a, b)?.ok_or_else(cold_overflow); + } + if ai == 0 { return Err(VmErr::Raised(String::from("ZeroDivisionError: zero to a negative power"))); } + // Clamp instead of wrapping so a huge negative exponent underflows to 0.0, base ±1 keeps its parity. + let r = match ai { + 1 => 1.0, + -1 => if exp % 2 == 0 { 1.0 } else { -1.0 }, + _ => libm::pow(ai as f64, exp as f64), + }; + return Ok(Val::float(r)); + } + if let Some(r) = self.wide_op(BinOp::Pow, a, b)? { return Ok(r); } + let (Some(fa), Some(fb)) = (num_f64(a, self.heap()), num_f64(b, self.heap())) else { return Err(cold_type(err_msg)); }; + let (fa, fb) = (fa?, fb?); + if fa == 0.0 && fb < 0.0 { return Err(VmErr::Raised(String::from("ZeroDivisionError: zero to a negative power"))); } + // A negative base under a fractional power has only a complex result. + if crate::value::math::complex_power(fa, fb) { return crate::frontend::complex::power(self.heap_mut(), fa, fb); } + fpowf(fa, fb).map(Val::float).ok_or_else(|| VmErr::Raised(String::from("OverflowError: (34, 'Numerical result out of range')"))) + } + + + /* True when a comparison over `vals` can reach a user dunder. */ + fn any_instance(&self, vals: &[Val]) -> bool { + vals.iter().any(|&v| matches!(self.heap().try_get(v), Some(HeapObj::Instance(..)))) + } + + /* Whether `f` is `eval` or `exec`, whose call where a frame may open runs its text as a floor of the loop. */ + fn runs_text(&self, f: Val) -> bool { matches!(self.heap().try_get(f), Some(HeapObj::NativeFn(NativeFnId::Eval | NativeFnId::Exec))) } + + #[inline] fn alloc_and_push_tuple(&mut self, items: Vec) -> Result<(), VmErr> { let v = self.heap_mut().alloc(HeapObj::Tuple(items))?; self.push(v); Ok(()) } +} + +// Type name and truth read off the heap alone. +impl HeapPool { + pub fn truthy(&self, v: Val) -> bool { + if v.is_none() || v.is_false() { return false; } + if v.is_true() { return true; } + if v.is_int() { return v.as_int() != 0; } + if v.is_float() { return v.as_float() != 0.0; } + if v.is_foreign() { return self.foreign_truthy(v).unwrap_or(true); } + match self.get(v) { + &HeapObj::Boxed(n) => self.truthy(n), + HeapObj::Str(s) => !s.is_empty(), + HeapObj::Bytes(b) | HeapObj::ByteArray(b) => !b.is_empty(), + HeapObj::MemoryView(m) => m.len > 0, + HeapObj::FnCode(_) => true, + HeapObj::List(l) => !l.borrow().is_empty(), + HeapObj::Tuple(t) => !t.is_empty(), + HeapObj::Dict(d) | HeapObj::FrozenDict(d) => !d.borrow().is_empty(), + HeapObj::Set(s) => !s.borrow().is_empty(), + HeapObj::FrozenSet(s) => !s.is_empty(), + HeapObj::Range(s,e,st) => if *st > 0 { s < e } else { s > e }, + HeapObj::BigRange(..) => crate::frontend::int::range_empty(self, v) == Some(false), + HeapObj::DictView(d, _) => self.truthy(*d), + HeapObj::Type(_) | HeapObj::Func(..) | HeapObj::Slice(..) | HeapObj::BoundMethod(..) + | HeapObj::NativeFn(_) | HeapObj::Class(..) | HeapObj::BoundUserMethod(..) + | HeapObj::Super(..) | HeapObj::Property(..) | HeapObj::PropertyPart(..) + | HeapObj::StaticMethod(..) | HeapObj::ClassMethod(..) | HeapObj::Instance(..) | HeapObj::Coroutine(..) + | HeapObj::Module(..) | HeapObj::Extern(_) | HeapObj::ExcInstance(..) + | HeapObj::Ellipsis | HeapObj::NotImplemented | HeapObj::GenericAlias(..) | HeapObj::TypeAlias(..) + | HeapObj::Union(_) | HeapObj::TypeVar(_) | HeapObj::Iter(..) | HeapObj::Cell(_) | HeapObj::Code(_) => true, + } + } + + #[inline] + pub fn type_name(&self, v: Val) -> &str { self.type_name_by(v, &|_| "coroutine") } + + // `coro` names a generator or coroutine by the body it runs. + #[inline(always)] + pub fn type_name_by<'h>(&'h self, v: Val, coro: &impl Fn(&super::Coro) -> &'h str) -> &'h str { + let scalar = |v: Val| if v.is_bool() { Some("bool") } + else if v.is_int() { Some("int") } + else if v.is_float() { Some("float") } + else if v.is_none() { Some("NoneType") } + else if v.is_foreign() { Some(self.foreign_kind(v).map_or("object", |k| k.type_name)) } + else { None }; + if let Some(n) = scalar(v) { n } + else { match self.get(v) { + // A number of a class over int or float takes the number's name. + &HeapObj::Boxed(n) => scalar(n).unwrap_or("object"), + HeapObj::Str(_) => "str", + HeapObj::Bytes(_) => "bytes", + HeapObj::ByteArray(_) => "bytearray", + HeapObj::MemoryView(_) => "memoryview", + HeapObj::FnCode(_) => "code", + HeapObj::List(_) => "list", + HeapObj::Dict(_) => "dict", + HeapObj::FrozenDict(_) => "frozendict", + HeapObj::Set(_) => "set", + HeapObj::FrozenSet(_) => "frozenset", + HeapObj::Tuple(_) => "tuple", + HeapObj::Func(..) => "function", + HeapObj::Type(_) | HeapObj::Class(..) => "type", + HeapObj::Range(..) | HeapObj::BigRange(..) => "range", + HeapObj::Slice(..) => "slice", + // A dunder of a builtin type is a slot wrapper, unbound or bound, a named method a descriptor until bound. + &HeapObj::BoundMethod(recv, id) if recv.is_undef() || id.ty() == "op" && matches!(self.try_get(recv), Some(HeapObj::Type(_))) => if is_dunder(id.name()) { "wrapper_descriptor" } else { "method_descriptor" }, + HeapObj::BoundMethod(_, id) if is_dunder(id.name()) => "method-wrapper", + HeapObj::BoundMethod(..) | HeapObj::NativeFn(_) | HeapObj::Extern(_) => "builtin_function_or_method", + HeapObj::BoundUserMethod(..) => "method", + HeapObj::Super(..) => "super", + HeapObj::Property(..) | HeapObj::PropertyPart(..) => "property", + HeapObj::StaticMethod(..) => "staticmethod", + HeapObj::ClassMethod(..) => "classmethod", + HeapObj::Instance(cls, _) => { + if cls.is_heap() && let HeapObj::Class(name, _, _) = self.get(*cls) { + return name; + } + "object" + } + HeapObj::Coroutine(c) => coro(c), + HeapObj::Module(..) => "module", + HeapObj::ExcInstance(..) => "exception", + HeapObj::Ellipsis => "ellipsis", + HeapObj::NotImplemented => "NotImplementedType", + HeapObj::GenericAlias(..) => "GenericAlias", + HeapObj::TypeAlias(..) => "TypeAliasType", + HeapObj::Union(_) => "Union", + // The stars a type parameter was declared with name its kind. + HeapObj::TypeVar(n) if n.starts_with("**") => "ParamSpec", + HeapObj::TypeVar(n) if n.starts_with('*') => "TypeVarTuple", + HeapObj::TypeVar(_) => "TypeVar", + HeapObj::Iter(_, name) => name, + HeapObj::Cell(_) => "cell", + HeapObj::Code(_) => "code", + HeapObj::DictView(_, View::Keys) => "dict_keys", + HeapObj::DictView(_, View::Values) => "dict_values", + HeapObj::DictView(_, View::Items) => "dict_items", + }} + } +} + +pub fn is_dunder(name: &str) -> bool { name.len() > 4 && name.starts_with("__") && name.ends_with("__") } + +pub(crate) fn normalize_index(i: i64, len: usize) -> usize { + (if i < 0 { len as i64 + i } else { i }) as usize +} + +/* Slice bounds clamped to a sequence of `len` items, the triple `slice.indices(len)` returns. */ +pub(crate) fn slice_bounds(start: Val, stop: Val, step: Val, len: i64) -> Result<(i64, i64, i64), VmErr> { + let st = if step.is_none() { 1 } else if step.is_int() { step.as_int() } else { + return Err(cold_type("slice step must be an integer")); + }; + if st == 0 { return Err(cold_value("slice step cannot be zero")); } + let clamp = |v: Val, def: i64| -> i64 { + if v.is_none() { def } + else if v.is_int() { let i = v.as_int(); if i < 0 { (len+i).max(0) } else { i.min(len) } } + else { def } + }; + // Negative step bounds at [-1, len-1], an underflowing index floors at -1, not 0. + let clamp_neg = |v: Val, def: i64| -> i64 { + if v.is_none() { def } + else if v.is_int() { let i = v.as_int(); (if i < 0 { len + i } else { i }).clamp(-1, len - 1) } + else { def } + }; + Ok(if st > 0 { (clamp(start, 0), clamp(stop, len), st) } else { (clamp_neg(start, len - 1), clamp_neg(stop, -1), st) }) +} + +pub(crate) fn released() -> VmErr { cold_value("operation forbidden on released memoryview object") } + +/* Same cap for `<` descent, self-referential sequences raise RecursionError. */ +pub(crate) const CMP_DEPTH_MAX: usize = 100; + +/* Coerce a numeric pair to f64, returns None if neither operand is a float. */ +pub(crate) fn coerce_floats(a: Val, b: Val, heap: &HeapPool) -> Option> { + if !a.is_float() && !b.is_float() { return None; } + let (x, y) = (num_f64(a, heap)?, num_f64(b, heap)?); + Some(x.and_then(|x| Ok((x, y?)))) +} + +/* `base ** e` by squaring, None once a step leaves i128. */ +pub(crate) fn checked_pow(mut base: i128, mut e: u64) -> Option { + let mut acc: i128 = 1; + while e > 0 { + if e & 1 == 1 { acc = acc.checked_mul(base)?; } + e >>= 1; + if e > 0 { base = base.checked_mul(base)?; } + } + Some(acc) +} diff --git a/src/value/mod.rs b/src/value/mod.rs index cb8d955d..ab7f1c7d 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -10,6 +10,11 @@ pub mod scheduler; pub mod meter; pub mod pool; pub mod foreign; +pub mod host; +pub mod render; +pub use host::Host; +// Python ints past what an i64 holds. +pub use crate::frontend::bigint::BigInt; pub use foreign::{BinOp, UnOp}; pub use frames::*; @@ -287,7 +292,7 @@ pub use crate::vm::methods::BuiltinMethodId; // One entry per builtin with its name and arity, `n`, `(lo, hi)`, `(lo, var)` or `var`. macro_rules! builtins { - ( $( $variant:ident => $name:literal, $arity:tt );* $(;)? ) => { + ( $( $variant:ident => $name:literal, $arity:tt $(, $why:literal)? $(=> $body:expr)? );* $(;)? ) => { #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] pub enum NativeFnId { $( $variant ),* } @@ -303,8 +308,23 @@ macro_rules! builtins { pub fn takes(self, n: u16) -> bool { let (lo, hi) = self.arity(); (lo..=hi).contains(&n) } // Every builtin, in table order. pub const ALL: &'static [NativeFnId] = &[ $( NativeFnId::$variant ),* ]; + // Whether the result hangs on the arguments alone. + pub fn pure(self) -> bool { match self { $( NativeFnId::$variant => builtins!(@b $($body)?) ),* } } + // Why call_pure leaves it out. + pub fn excluded(self) -> Option<&'static str> { match self { $( NativeFnId::$variant => builtins!(@w $($why)?) ),* } } + // Runs on plain values with `n` arguments on the stack. + #[cfg(feature = "pure")] + pub(crate) fn plain(self, h: &mut crate::pure::PlainHost, n: u16) -> Option> { + #[allow(unused_imports)] + use crate::vm::natives::Builtins; + match self { $( NativeFnId::$variant => builtins!(@p h n $($body)?) ),* } + } } }; + (@b) => { false }; (@b $body:expr) => { true }; + (@w) => { None }; (@w $why:literal) => { Some($why) }; + (@p $h:ident $n:ident) => { None }; + (@p $h:ident $n:ident $body:expr) => {{ let f: fn(&mut crate::pure::PlainHost, u16, &()) -> Result<(), VmErr> = $body; Some(f($h, $n, &())) }}; (@a var) => { (0, u16::MAX) }; (@a ($lo:literal, var)) => { ($lo, u16::MAX) }; (@a ($lo:literal, $hi:literal)) => { ($lo, $hi) }; @@ -312,45 +332,85 @@ macro_rules! builtins { } builtins! { - Print => "print", var; - Len => "len", 1; Abs => "abs", 1; Str => "str", (0, 3); Int => "int", (0, 2); Float => "float", (0, 1); - Bool => "bool", (0, 1); Type => "type", (1, 3); Chr => "chr", 1; Ord => "ord", 1; - Range => "range", (1, 3); Round => "round", (1, 2); Min => "min", (1, var); Max => "max", (1, var); Sum => "sum", (1, 2); - Sorted => "sorted", 1; Enumerate => "enumerate", (1, 2); Zip => "zip", var; - List => "list", (0, 1); Tuple => "tuple", (0, 1); Dict => "dict", (0, 1); Set => "set", (0, 1); - IsInstance => "isinstance", 2; IsSubclass => "issubclass", 2; Input => "input", 0; - All => "all", 1; Any => "any", 1; - Bin => "bin", 1; Oct => "oct", 1; Hex => "hex", 1; Divmod => "divmod", 2; Pow => "pow", (2, 3); - Repr => "repr", 1; Reversed => "reversed", 1; Callable => "callable", 1; - Format => "format", (1, 2); GetAttr => "getattr", (2, 3); HasAttr => "hasattr", 2; SetAttr => "setattr", 3; DelAttr => "delattr", 2; - Next => "next", (1, 2); - Receive => "receive", 0; Map => "map", (2, var); Filter => "filter", 2; Iter => "iter", (1, 2); - Bytes => "bytes", (0, 3); ImportModule => "import_module", 1; Slice => "slice", (1, 3); Vars => "vars", (0, 1); - BytesFromHex => "bytes_fromhex", 1; IntFromBytes => "int_from_bytes", 2; IntToBytes => "int_to_bytes", 3; FrozenSet => "frozenset", (0, 1); - Globals => "globals", 0; Locals => "locals", 0; Dir => "dir", (0, 1); - Super => "super", (0, 2); - Property => "property", var; - StaticMethod => "staticmethod", 1; - ClassMethod => "classmethod", 1; - SendMsg => "send", 2; - Eval => "eval", (1, 3); - Exec => "exec", (1, 3); - Compile => "compile", (3, 6); - Id => "id", 1; - Hash => "hash", 1; - Complex => "complex", (0, 2); - ByteArray => "bytearray", (0, 3); - MemoryView => "memoryview", 1; - Ascii => "ascii", 1; - Sentinel => "sentinel", 1; - AIter => "aiter", 1; - ANext => "anext", (1, 2); - FrozenDict => "frozendict", (0, 1); + Print => "print", var, "writes to the output of the program"; + Len => "len", 1 => |h, _, c| h.call_len(c); + Abs => "abs", 1 => |h, _, c| h.call_abs(c); + Str => "str", (0, 3) => |h, n, c| h.call_str(n, c); + Int => "int", (0, 2) => |h, n, c| h.call_int(n, c); + Float => "float", (0, 1) => |h, n, c| h.call_float(n, c); + Bool => "bool", (0, 1) => |h, n, c| h.call_bool(n, c); + Type => "type", (1, 3), "gives a type object, which is no plain value"; + Chr => "chr", 1 => |h, _, _| h.call_chr(); + Ord => "ord", 1 => |h, _, _| h.call_ord(); + Range => "range", (1, 3) => |h, n, _| h.call_range(n); + Round => "round", (1, 2) => |h, n, _| h.call_round(n); + Min => "min", (1, var) => |h, n, c| h.call_min(n, c); + Max => "max", (1, var) => |h, n, c| h.call_max(n, c); + Sum => "sum", (1, 2) => |h, n, c| h.call_sum(n, c); + Sorted => "sorted", 1 => |h, _, c| h.sorted_of(false, false, c); + Enumerate => "enumerate", (1, 2) => |h, n, c| h.call_enumerate(n, c); + Zip => "zip", var => |h, n, c| h.call_zip(n, c); + List => "list", (0, 1) => |h, n, c| h.call_list(n, c); + Tuple => "tuple", (0, 1) => |h, n, c| h.call_tuple(n, c); + Dict => "dict", (0, 1) => |h, n, _| h.call_dict(n); + Set => "set", (0, 1) => |h, n, _| h.call_set(n); + IsInstance => "isinstance", 2, "takes type objects, which are no plain values"; + IsSubclass => "issubclass", 2, "takes type objects, which are no plain values"; + Input => "input", 0, "reads the input of the program"; + All => "all", 1 => |h, n, c| h.call_all(n, c); + Any => "any", 1 => |h, n, c| h.call_any(n, c); + Bin => "bin", 1 => |h, _, _| h.call_bin(); + Oct => "oct", 1 => |h, _, _| h.call_oct(); + Hex => "hex", 1 => |h, _, _| h.call_hex(); + Divmod => "divmod", 2 => |h, _, _| h.call_divmod(); + Pow => "pow", (2, 3) => |h, n, _| h.call_pow(n); + Repr => "repr", 1 => |h, _, c| h.call_repr(c); + Reversed => "reversed", 1 => |h, _, _| h.call_reversed(); + Callable => "callable", 1 => |h, _, _| h.call_callable(); + Format => "format", (1, 2) => |h, n, c| h.call_format(n, c); + GetAttr => "getattr", (2, 3), "reads attributes, which may run user code"; + HasAttr => "hasattr", 2, "reads attributes, which may run user code"; + SetAttr => "setattr", 3, "changes an object"; + DelAttr => "delattr", 2, "changes an object"; + Next => "next", (1, 2), "steps an iterator the VM holds"; + Receive => "receive", 0, "waits for an event of the host"; + Map => "map", (2, var), "calls a function, which may be user code"; + Filter => "filter", 2, "calls a function, which may be user code"; + Iter => "iter", (1, 2), "gives an iterator the VM holds"; + Bytes => "bytes", (0, 3) => |h, n, _| h.call_bytes(n); + ImportModule => "import_module", 1, "reads the modules of the program"; + Slice => "slice", (1, 3) => |h, n, _| h.call_slice(n); + Vars => "vars", (0, 1), "reads the namespace of a frame"; + BytesFromHex => "bytes_fromhex", 1 => |h, _, _| h.call_bytes_fromhex(); + IntFromBytes => "int_from_bytes", 2 => |h, _, _| h.call_int_from_bytes(); + IntToBytes => "int_to_bytes", 3 => |h, _, _| h.call_int_to_bytes(); + FrozenSet => "frozenset", (0, 1) => |h, n, _| h.call_frozenset(n); + Globals => "globals", 0, "reads the namespace of a frame"; + Locals => "locals", 0, "reads the namespace of a frame"; + Dir => "dir", (0, 1), "reads the names of a frame or an object"; + Super => "super", (0, 2), "reads the class and frame it runs in"; + Property => "property", var, "makes a descriptor for a class"; + StaticMethod => "staticmethod", 1, "makes a descriptor for a class"; + ClassMethod => "classmethod", 1, "makes a descriptor for a class"; + SendMsg => "send", 2, "hands a message to the host"; + Eval => "eval", (1, 3), "compiles and runs code"; + Exec => "exec", (1, 3), "compiles and runs code"; + Compile => "compile", (3, 6), "compiles code"; + Id => "id", 1, "reads where the VM placed a value"; + Hash => "hash", 1, "gives a hash the VM seeds"; + Complex => "complex", (0, 2) => |h, n, c| h.call_complex(n, c); + ByteArray => "bytearray", (0, 3) => |h, n, _| h.call_bytearray(n); + MemoryView => "memoryview", 1, "shares the buffer of the VM that holds it"; + Ascii => "ascii", 1 => |h, _, c| h.call_ascii(c); + Sentinel => "sentinel", 1, "makes an object equal only to itself"; + AIter => "aiter", 1, "gives an async iterator the VM steps"; + ANext => "anext", (1, 2), "steps an async iterator the VM holds"; + FrozenDict => "frozendict", (0, 1) => |h, n, _| h.call_frozendict(n); // The steps `except*` compiles to, named so no program can spell them. - StarStart => "%star_start", 1; - StarSplit => "%star_split", 2; - StarRaised => "%star_raised", 3; - StarEnd => "%star_end", 1; + StarStart => "%star_start", 1, "a step of except*, which only compiled code takes"; + StarSplit => "%star_split", 2, "a step of except*, which only compiled code takes"; + StarRaised => "%star_raised", 3, "a step of except*, which only compiled code takes"; + StarEnd => "%star_end", 1, "a step of except*, which only compiled code takes"; } /* Insertion-ordered set, each item beside its hash and removed ones undef tombstones, so no table layout decides its order. */ diff --git a/src/value/render.rs b/src/value/render.rs new file mode 100644 index 00000000..07e17b70 --- /dev/null +++ b/src/value/render.rs @@ -0,0 +1,70 @@ +use alloc::string::String; +use super::Val; + +/* Cap on nested-container rendering depth, stops self-referential prints from overflowing the stack. */ +pub(crate) const RENDER_DEPTH_MAX: usize = 100; +/* Cap on total rendered output, bounds breadth the way RENDER_DEPTH_MAX bounds depth. */ +pub(crate) const MAX_REPR_LEN: usize = 1_000_000; + +/* Render `bytes` as `b'...'`, printable ASCII verbatim and the rest escaped, in double quotes when it holds a single quote and no double one. */ +pub(crate) fn format_bytes(buf: &[u8]) -> String { quote_bytes(buf, false) } + +/* `format_bytes`, the single quote escaped even inside double quotes when `escape_single` asks, as a bytearray shows. */ +pub(crate) fn quote_bytes(buf: &[u8], escape_single: bool) -> String { + let quote = if buf.contains(&b'\'') && !buf.contains(&b'"') { b'"' } else { b'\'' }; + let mut out = String::with_capacity(buf.len() + 3); + out.push('b'); + out.push(quote as char); + for &b in buf { + match b { + b'\\' => out.push_str("\\\\"), + b'\'' if quote == b'\'' || escape_single => out.push_str("\\'"), + b'\n' => out.push_str("\\n"), + b'\r' => out.push_str("\\r"), + b'\t' => out.push_str("\\t"), + 0x20..=0x7E => out.push(b as char), + _ => { + out.push_str("\\x"); + const HEX: &[u8; 16] = b"0123456789abcdef"; + out.push(HEX[(b >> 4) as usize] as char); + out.push(HEX[(b & 0x0F) as usize] as char); + } + } + } + out.push(quote as char); + out +} + +/* `repr` of a str, quote selection (' unless the text has ' but not ") and backslash escapes for what is not printable, the rest verbatim. */ +pub(crate) fn repr_str(s: &str) -> String { + let mut out = String::with_capacity(s.len() + 2); + push_repr_str(&mut out, s); + out +} + +/* `repr_str` of `s` written onto `out`. */ +pub(crate) fn push_repr_str(out: &mut String, s: &str) { + use core::fmt::Write; + let quote = if s.contains('\'') && !s.contains('"') { '"' } else { '\'' }; + out.push(quote); + for c in s.chars() { + match c { + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + c if c == quote => { out.push('\\'); out.push(c); } + c if !crate::util::uni::is_printable(c) => { + let n = c as u32; + if n <= 0xff { let _ = write!(out, "\\x{:02x}", n); } + else if n <= 0xffff { let _ = write!(out, "\\u{:04x}", n); } + else { let _ = write!(out, "\\U{:08x}", n); } + } + c => out.push(c), + } + } + out.push(quote); +} + +/* The ` at 0x...` a repr places an object by, the word `id` gives. */ +pub(crate) fn at(v: Val) -> String { alloc::format!(" at 0x{:x}", v.0) } diff --git a/src/vm/dispatch.rs b/src/vm/dispatch.rs index fb63703d..3d79aaed 100644 --- a/src/vm/dispatch.rs +++ b/src/vm/dispatch.rs @@ -12,6 +12,7 @@ use super::lower::{self, Code, Ins, STORE_NOTES_BUILTIN}; use super::scope::Kind; use super::opcodes::{attr_lookup::AttrLookup, function::{Args, Passed}}; use super::registers::{int_add, int_sub, int_mul, int_mod, int_div, int_floordiv, float_div, int_bits, numeric_binop}; +use crate::vm::natives::Builtins; /* The frame an instruction loop began with, which the frames it opens return to last. */ pub(crate) struct Floor { diff --git a/src/vm/globals/bytes_helpers.rs b/src/vm/globals/bytes_helpers.rs index 3ed8be6e..0f193c0a 100644 --- a/src/vm/globals/bytes_helpers.rs +++ b/src/vm/globals/bytes_helpers.rs @@ -5,25 +5,8 @@ use super::super::types::*; impl<'a> VM<'a> { - /* `bytes_fromhex(s)`, the `bytes.fromhex` classmethod as a function. */ - pub fn call_bytes_fromhex(&mut self) -> Result<(), VmErr> { - let s = self.pop()?; - crate::vm::methods::bytes::fromhex(self, Val::none(), &[s]) - } - /* `int_from_bytes(b, byteorder)`, the `int.from_bytes` classmethod as a function. */ - pub fn call_int_from_bytes(&mut self) -> Result<(), VmErr> { - let order = self.pop()?; - let b = self.pop()?; - if !matches!(self.heap.try_get(b), Some(HeapObj::Bytes(_))) { return Err(cold_type("int_from_bytes() first arg must be bytes")); } - crate::vm::methods::numeric::from_bytes(self, Val::none(), &[b, order]) - } - // `int_to_bytes(n, length, byteorder)`, the `int.to_bytes` method as a function. - pub fn call_int_to_bytes(&mut self) -> Result<(), VmErr> { - let args = self.pop_n(3)?; - crate::vm::methods::numeric::to_bytes(self, args[0], &args[1..]) - } // `import_module(name)`, fetch an already-imported module by alias, returns its `HeapObj::Module` Val. pub fn call_import_module(&mut self) -> Result<(), VmErr> { diff --git a/src/vm/globals/compile.rs b/src/vm/globals/compile.rs index ffb46296..285f8f8f 100644 --- a/src/vm/globals/compile.rs +++ b/src/vm/globals/compile.rs @@ -10,6 +10,7 @@ use core::hash::Hasher; use super::super::VM; use super::super::types::*; +use crate::vm::natives::Builtins; /* A chunk compiled while the program runs, kept while the VM lives since the functions it defines point into it. */ pub(crate) struct Compiled { @@ -364,8 +365,6 @@ impl<'a> VM<'a> { } } - /* Whether `f` is `eval` or `exec`, whose call where a frame may open runs its text as a floor of the loop. */ - pub(crate) fn runs_text(&self, f: Val) -> bool { matches!(self.heap.try_get(f), Some(HeapObj::NativeFn(NativeFnId::Eval | NativeFnId::Exec))) } /* Marks the run of text last set aside as the callback of what `fed` names, which takes what it gives. */ pub(crate) fn feed_floor(&mut self, fed: Fed) { diff --git a/src/vm/globals/container.rs b/src/vm/globals/container.rs index 83d4030e..086cbb61 100644 --- a/src/vm/globals/container.rs +++ b/src/vm/globals/container.rs @@ -1,61 +1,22 @@ use core::cell::RefCell; -use alloc::{rc::Rc, string::String, vec::Vec}; +use alloc::rc::Rc; use super::super::VM; use super::super::types::*; impl<'a> VM<'a> { - /* Heap-alloc `s` and push the resulting Val. Used by builtins that produce string results. */ - pub(crate) fn alloc_and_push_str(&mut self, s: String) -> Result<(), VmErr> { - let v = self.heap.alloc(HeapObj::Str(s))?; - self.push(v); Ok(()) - } - - /* Allocate a List from items and push. Centralises the Rc::new(RefCell::new(items)) construction inlined. */ - pub(crate) fn alloc_list(&mut self, items: Vec) -> Result { - self.heap.alloc(HeapObj::List(Rc::new(RefCell::new(items)))) - } - - /* Allocate a List, push it, return Ok. */ - pub(crate) fn alloc_and_push_list(&mut self, items: Vec) -> Result<(), VmErr> { - let v = self.alloc_list(items)?; - self.push(v); Ok(()) - } - - // Allocate a Set from `items` (deduped by content) and push. Mirrors `alloc_and_push_list`. - pub(crate) fn alloc_and_push_set(&mut self, items: Vec) -> Result<(), VmErr> { - let v = self.alloc_set_result(items, false)?; - self.push(v); Ok(()) - } - - /* Allocate a Dict from a DictMap and push. */ - pub(crate) fn alloc_and_push_dict(&mut self, dm: DictMap) -> Result<(), VmErr> { - let v = self.heap.alloc(HeapObj::Dict(Rc::new(RefCell::new(dm))))?; - self.push(v); Ok(()) - } - - /* Allocate a Tuple and push. */ - pub(crate) fn alloc_and_push_tuple(&mut self, items: Vec) -> Result<(), VmErr> { - let v = self.tuple_from_items(items)?; - self.push(v); Ok(()) - } - - // Build a tuple Val from items. Shared by the VM and the plugin ABI. - pub(crate) fn tuple_from_items(&mut self, items: Vec) -> Result { - self.heap.alloc(HeapObj::Tuple(items)) - } // Build a set Val from items, rejecting unhashable elements first. #[cfg(all(target_arch = "wasm32", feature = "runtime"))] - pub(crate) fn set_from_items(&mut self, items: Vec) -> Result { + pub(crate) fn set_from_items(&mut self, items: alloc::vec::Vec) -> Result { for &v in items.iter().filter(|v| v.is_heap()) { self.require_element(v)?; } self.alloc_set_result(items, false) } // Build a frozenset Val from items, rejecting unhashable elements first. #[cfg(all(target_arch = "wasm32", feature = "runtime"))] - pub(crate) fn frozenset_from_items(&mut self, items: Vec) -> Result { + pub(crate) fn frozenset_from_items(&mut self, items: alloc::vec::Vec) -> Result { for &v in items.iter().filter(|v| v.is_heap()) { self.require_element(v)?; } self.alloc_set_result(items, true) } @@ -76,136 +37,12 @@ impl<'a> VM<'a> { self.push(val); Ok(()) } - // Operand packs kw<<8 | pos, keep counts distinct. - pub fn call_dict(&mut self, op: u16) -> Result<(), VmErr> { - let pos = (op & 0xFF) as usize; - let kw = (op >> 8) as usize; - if pos > 1 { - return Err(cold_type("dict expected at most 1 argument")); - } - // Keyword pairs sit above the positional source. - let ok = core::mem::take(&mut self.job_ok); - let kw_flat = self.pop_n(kw * 2)?; - let src = if pos == 1 { Some(self.pop()?) } else { None }; - // A generator called where a frame may open runs as a frame of the loop, its pairs gathered as it yields them. - if ok && kw == 0 && let Some(s) = src && s.is_heap() && self.gather_gen(s, crate::vm::jobs::Takes::All(crate::parser::OpCode::CallDict, op), &[])? { return Ok(()); } - // A dict source copies with the hashes it stored. - if let Some(rc) = src.and_then(|s| self.heap.dict_cell(s)) && kw_flat.is_empty() { - let dm = rc.borrow().clone(); - return self.alloc_and_push_dict(dm); - } - let mut pairs = match src { Some(s) => self.pairs_of(s)?, None => Vec::new() }; - pairs.extend(kw_flat.chunks(2).map(|p| (p[0], p[1]))); - let dm = self.dictmap_of(pairs)?; - self.alloc_and_push_dict(dm) - } - /* `frozendict()` as `dict()` builds it, a frozendict alone given back as itself. */ - pub fn call_frozendict(&mut self, op: u16) -> Result<(), VmErr> { - if op == 1 && let Some(&top) = self.stack.last() && matches!(self.heap.try_get(top), Some(HeapObj::FrozenDict(_))) { return Ok(()); } - self.call_dict(op)?; - let made = *self.stack.last().ok_or_else(|| cold_runtime("stack underflow"))?; - if let Some(slot) = self.heap.try_get_mut(made) && let HeapObj::Dict(rc) = slot { *slot = HeapObj::FrozenDict(rc.clone()); } - Ok(()) - } - /* Entries of a mapping, or of an iterable of two-item iterables, as `dict()` takes them. */ - pub(crate) fn pairs_of(&mut self, src: Val) -> Result, VmErr> { - if let Some(rc) = self.heap.dict_cell(src) { self.heap.spend(rc.borrow().len()); return Ok(rc.borrow().iter().collect()); } - let mut pairs = Vec::new(); - for item in self.extract_iter(src)? { - let item = self.extract_iter(item).map_err(|e| match e { - VmErr::TypeMsg(_) => VmErr::TypeMsg("object is not iterable".into()), - e => e, - })?; - let [k, v] = item[..] else { - return Err(VmErr::Raised(crate::s!("ValueError: dictionary update sequence element #", int pairs.len() as i64, " has length ", int item.len() as i64, "; 2 is required"))); - }; - self.require_key(k)?; - self.keyed(k)?; - pairs.push((k, v)); - } - Ok(pairs) - } - pub fn call_set(&mut self, op: u16) -> Result<(), VmErr> { - let ok = core::mem::take(&mut self.job_ok); - let src = if op == 0 { None } else { Some(self.pop()?) }; - if ok && let Some(s) = src && s.is_heap() && self.gather_gen(s, crate::vm::jobs::Takes::All(crate::parser::OpCode::CallSet, 1), &[])? { return Ok(()); } - let s = self.set_source(src)?; - let val = self.heap.alloc(HeapObj::Set(Rc::new(RefCell::new(s))))?; - self.push(val); - Ok(()) - } - /* The items `set(src)` or `frozenset(src)` holds, a set source copies the hashes it stored. */ - fn set_source(&mut self, src: Option) -> Result { - let Some(src) = src else { return Ok(ValSet::new()) }; - match self.heap.try_get(src) { - Some(HeapObj::Set(rc)) => return Ok(rc.borrow().clone()), - Some(HeapObj::FrozenSet(rc)) => return Ok((**rc).clone()), - _ => {} - } - let items = self.extract_iter(src)?; - self.valset_of(&items) - } - /* `frozenset()` | `frozenset(iter)`, construct an immutable, hashable set from an iterable. Without args returns the empty frozenset. */ - pub fn call_frozenset(&mut self, argc: u16) -> Result<(), VmErr> { - let args = self.pop_n(argc as usize)?; - if args.len() > 1 { return Err(cold_type("frozenset() takes 0 or 1 argument")); } - let s = self.set_source(args.first().copied())?; - let v = self.heap.alloc(HeapObj::FrozenSet(Rc::new(s)))?; - self.push(v); Ok(()) - } - /* `bytes()`, empty, `n` zero bytes, an iterable of ints in range(256), a bytes-like value or `(str, encoding, errors)`. */ - #[inline(never)] - pub fn call_bytes(&mut self, argc: u16) -> Result<(), VmErr> { - let args = self.pop_n(argc as usize)?; - let buf = self.octets_of(&args, "bytes")?; - let v = self.heap.alloc(HeapObj::Bytes(buf))?; - self.push(v); Ok(()) - } - /* `bytearray(...)`, the arguments `bytes()` takes, a mutable copy. */ - pub fn call_bytearray(&mut self, argc: u16) -> Result<(), VmErr> { - let args = self.pop_n(argc as usize)?; - let buf = self.octets_of(&args, "bytearray")?; - let v = self.heap.alloc(HeapObj::ByteArray(buf))?; - self.push(v); Ok(()) - } - /* The octets the arguments of `bytes()` or `bytearray()` name. */ - fn octets_of(&mut self, args: &[Val], what: &str) -> Result, VmErr> { - Ok(match *args { - [] => Vec::new(), - [a] if a.is_int() || a.is_bool() || crate::frontend::int::is_big(&self.heap, a) => { - let n = self.as_i128(a).unwrap_or(i128::MAX); - if n < 0 { return Err(cold_value("negative count")); } - // Length is user-controlled, cap it against the memory left so a huge count errors instead of aborting. - let n = usize::try_from(n).map_err(|_| cold_overflow())?; - self.heap.reserve(n)?; - alloc::vec![0u8; n] - } - [a] if self.heap.bytes_of(a).is_some() => self.heap.bytes_of(a).unwrap_or_default().to_vec(), - [a] if matches!(self.heap.try_get(a), Some(HeapObj::Str(_))) => return Err(VmErr::TypeMsg(crate::s!("string argument without an encoding"))), - [a] => { - let items = self.extract_iter(a).map_err(|_| VmErr::TypeMsg(crate::s!("cannot convert '", str self.type_name(a), "' object to ", str what)))?; - // `bytes` names its range in the plural, `bytearray` in the singular. - items.into_iter().map(|v| self.byte_value(v).map_err(|e| if what == "bytes" && matches!(e, VmErr::Value(_)) { cold_value("bytes must be in range(0, 256)") } else { e })).collect::, VmErr>>()? - } - [text, ref rest @ ..] if rest.len() <= 2 => { - for (&a, name) in rest.iter().zip(["encoding", "errors"]) { - if !matches!(self.heap.try_get(a), Some(HeapObj::Str(_))) { - return Err(VmErr::TypeMsg(crate::s!(str what, "() argument '", str name, "' must be str, not ", str self.type_name(a)))); - } - } - let Some(HeapObj::Str(t)) = self.heap.try_get(text) else { return Err(cold_type("encoding without a string argument")) }; - let (encoding, errors) = crate::vm::methods::string::codec_args(self, rest)?; - crate::vm::codecs::encode(t, encoding, errors)? - } - _ => return Err(VmErr::TypeMsg(crate::s!(str what, "() takes at most 3 arguments"))), - }) - } } diff --git a/src/vm/globals/conversion.rs b/src/vm/globals/conversion.rs index 3a457e54..6065baf4 100644 --- a/src/vm/globals/conversion.rs +++ b/src/vm/globals/conversion.rs @@ -1,47 +1,10 @@ use super::super::VM; use super::super::types::*; -use crate::alloc::string::ToString; +use crate::vm::natives::Builtins; impl<'a> VM<'a> { - pub fn call_str(&mut self, argc: u16, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { - let ok = core::mem::take(&mut self.job_ok); - if argc == 0 { return self.alloc_and_push_str(alloc::string::String::new()); } - if argc >= 2 { - // `str(bytes, encoding[, errors])` decodes, mirroring `bytes.decode`. - let args = self.pop_n(argc as usize)?; - for (&a, name) in args[1..].iter().zip(["encoding", "errors"]) { - if !matches!(self.heap.try_get(a), Some(HeapObj::Str(_))) { - return Err(VmErr::TypeMsg(crate::s!("str() argument '", str name, "' must be str, not ", str self.type_name(a)))); - } - } - if self.heap.bytes_of(args[0]).is_some() { - return crate::vm::methods::bytes::decode(self, args[0], &args[1..]); - } - return Err(VmErr::TypeMsg(crate::s!("decoding to str: need a bytes-like object, ", str self.type_name(args[0]), " found"))); - } - let o = self.pop()?; - // A container writes its items with repr, as `repr` does, and an instance its `__str__` or `__repr__`, each run as frames of the loop. - if ok && o.is_heap() && self.render_job(o, false, chunk)? { return Ok(()); } - if ok && self.is_instance(o) && self.str_job(o, false) { return Ok(()); } - let s = self.display_op(o, chunk)?; - self.alloc_and_push_str(s) - } - pub fn call_bool(&mut self, argc: u16, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { - let ok = core::mem::take(&mut self.job_ok); - if argc == 0 { self.push(Val::bool(false)); return Ok(()); } - let o = self.pop()?; - // An instance's `__bool__`, or past it its `__len__`, runs as a frame of the loop. - if ok && self.is_instance(o) { - let class = self.instance_class(o).unwrap_or(Val::undef()); - let has_bool = self.lookup_class_member(class, "__bool__").is_some(); - if if has_bool { self.check_job(o, crate::vm::jobs::Check::Bool) } else { self.check_job(o, crate::vm::jobs::Check::LenTruth) } { return Ok(()); } - } - let t = self.truthy_op(o, chunk)?; - self.push(Val::bool(t)); - Ok(()) - } pub fn call_type(&mut self, argc: u16) -> Result<(), VmErr> { let ok = core::mem::take(&mut self.job_ok); @@ -78,17 +41,4 @@ impl<'a> VM<'a> { Ok(()) } - /* The name `type(x)` reports, a user instance's own class, an exception's concrete class, else the builtin type name. */ - pub(crate) fn type_repr_name(&self, o: Val) -> alloc::string::String { - if o.is_heap() { - match self.heap.get(o) { - // Exception instances report their concrete class (e.g. `ZeroDivisionError`). - HeapObj::ExcInstance(n, ..) => return n.clone(), - _ => if let Some(cls) = self.instance_class(o) && let HeapObj::Class(n, _, _) = self.heap.get(cls) { - return n.clone(); - }, - } - } - self.type_name(o).to_string() // interned, so shares the `set`/`int` singleton - } } diff --git a/src/vm/globals/identity.rs b/src/vm/globals/identity.rs index 8daa8753..b4ba7994 100644 --- a/src/vm/globals/identity.rs +++ b/src/vm/globals/identity.rs @@ -82,22 +82,7 @@ impl<'a> VM<'a> { self.call_super(2) } - pub fn call_repr(&mut self, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { - let ok = core::mem::take(&mut self.job_ok); - let o = self.pop()?; - // A container of instances whose `__repr__` runs as frames of the loop writes there, as an instance's own does. - if ok && o.is_heap() && self.render_job(o, false, chunk)? { return Ok(()); } - if ok && self.is_instance(o) && self.check_job(o, crate::vm::jobs::Check::Str(true)) { return Ok(()); } - let s = self.repr_op(o, chunk)?; - self.alloc_and_push_str(s) - } - /* `ascii(x)`, the repr of `x` with every non-ASCII character escaped. */ - pub fn call_ascii(&mut self, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { - let o = self.pop()?; - let s = self.repr_op(o, chunk)?; - self.alloc_and_push_str(crate::vm::format_spec::ascii_escape(&s)) - } /* `sentinel(name, *, repr=None)`, a new object equal only to itself. */ pub fn call_sentinel(&mut self, op: u16) -> Result<(), VmErr> { @@ -120,20 +105,7 @@ impl<'a> VM<'a> { Ok(()) } - pub fn call_callable(&mut self) -> Result<(), VmErr> { - let o = self.pop()?; - let result = self.is_callable(o); - self.push(Val::bool(result)); - Ok(()) - } - /* Whether `o` can be called, an instance or a value a class made only when its class defines `__call__`. */ - pub(crate) fn is_callable(&self, o: Val) -> bool { - if let Some(cls) = self.instance_class(o) { return self.lookup_class_member(cls, "__call__").is_some(); } - matches!(self.heap.try_get(o), Some(HeapObj::Func(..) | HeapObj::BoundMethod(..) | HeapObj::Type(_) | HeapObj::NativeFn(_) - | HeapObj::Class(..) | HeapObj::BoundUserMethod(..) | HeapObj::Extern(_) | HeapObj::PropertyPart(..) - | HeapObj::StaticMethod(_) | HeapObj::GenericAlias(..))) - } /* Type-name based isinstance check. Accepts Type / NativeFn (builtin types) / user Class on the right, allows int<->bool aliasing and walks user inheritance via `is_subclass`. */ pub fn call_isinstance(&mut self) -> Result<(), VmErr> { diff --git a/src/vm/globals/io.rs b/src/vm/globals/io.rs index 32f60116..c33eaf8d 100644 --- a/src/vm/globals/io.rs +++ b/src/vm/globals/io.rs @@ -2,6 +2,7 @@ use alloc::string::String; use super::super::VM; use super::super::types::*; +use crate::vm::natives::Builtins; impl<'a> VM<'a> { @@ -106,21 +107,6 @@ impl<'a> VM<'a> { self.push(val); Ok(()) } - // `format(value [, spec])`. - pub fn call_format(&mut self, op: u16, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { - if op != 1 && op != 2 { - return Err(cold_type("format() takes 1 or 2 arguments")); - } - let spec_val = if op == 2 { Some(self.pop()?) } else { None }; - let val = self.pop()?; - let spec = match spec_val.map(|sv| self.heap.try_get(sv)) { - None => String::new(), - Some(Some(HeapObj::Str(s))) => s.clone(), - Some(_) => return Err(VmErr::TypeMsg(crate::s!("format() argument 2 must be str, not ", str self.type_name(spec_val.unwrap_or_else(Val::none))))), - }; - let result = self.format_op(val, &spec, chunk)?; - self.alloc_and_push_str(result) - } } /* What `input()` raises once no line is left to read. */ diff --git a/src/vm/globals/mod.rs b/src/vm/globals/mod.rs index b06e692e..950e37f2 100644 --- a/src/vm/globals/mod.rs +++ b/src/vm/globals/mod.rs @@ -40,7 +40,7 @@ pub(in crate::vm) fn is_exc_type(name: &str) -> bool { name == "BaseException" | /* Whether builtin exception `name` is a group, the two group types having no builtin subclass. */ #[inline] -pub(in crate::vm) fn is_group_type(name: &str) -> bool { matches!(name, "ExceptionGroup" | "BaseExceptionGroup") } +pub(crate) fn is_group_type(name: &str) -> bool { matches!(name, "ExceptionGroup" | "BaseExceptionGroup") } /* The second parent of a builtin exception with two, `ExceptionGroup` under `BaseExceptionGroup`. */ fn exc_also(name: &str) -> Option<&'static str> { @@ -58,7 +58,7 @@ pub(in crate::vm) fn type_parents(name: &str) -> alloc::vec::Vec<&'static str> { exc_parents(name) } -pub(in crate::vm) fn matches_exc_class(actual: &str, expected: &str) -> bool { +pub(crate) fn matches_exc_class(actual: &str, expected: &str) -> bool { let mut cur = actual; loop { if cur == expected { return true; } diff --git a/src/vm/globals/numeric.rs b/src/vm/globals/numeric.rs index cee5d3a7..d5a92b92 100644 --- a/src/vm/globals/numeric.rs +++ b/src/vm/globals/numeric.rs @@ -21,7 +21,7 @@ pub(crate) fn neumaier(sum: f64, comp: f64, x: f64) -> (f64, f64) { } /* `x` rounded half to even to a multiple of 10^k, reading every digit `{:.0}` prints. */ -fn round_float_tens(x: f64, k: usize) -> f64 { +pub(crate) fn round_float_tens(x: f64, k: usize) -> f64 { let t = libm::trunc(x); let mut d = alloc::format!("{:0>w$.0}", libm::fabs(t), w = k + 1).into_bytes(); let cut = d.len() - k; @@ -41,7 +41,7 @@ fn round_float_tens(x: f64, k: usize) -> f64 { } /* `int(s, base)` parsing, optional sign, optional 0x/0o/0b prefix (matching the base, or inferred when base==0), `_` digit separators, radix 0 or 2..=36. */ -fn parse_int_radix(heap: &mut HeapPool, s: &str, base: i64) -> Result { +pub(crate) fn parse_int_radix(heap: &mut HeapPool, s: &str, base: i64) -> Result { if base != 0 && !(2..=36).contains(&base) { return Err(cold_value("int() base must be >= 2 and <= 36, or 0")); } @@ -94,503 +94,43 @@ fn parse_int_radix(heap: &mut HeapPool, s: &str, base: i64) -> Result VM<'a> { - pub fn call_abs(&mut self, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { - let ok = core::mem::take(&mut self.job_ok); - let o = self.pop()?; - // An instance's `__abs__` runs as a frame of the loop. - if ok && self.is_instance(o) && self.check_job(o, crate::vm::jobs::Check::Abs) { return Ok(()); } - // Honor a user `__abs__` and pass any return type through. - if let Some(r) = self.try_call_dunder(o, "__abs__", &[], chunk)? { - self.push(r); - return Ok(()); - } - let v = if o.is_float() { - Val::float(o.as_float().abs()) - } else if o.is_int() || o.is_bool() { - Val::int(self.as_i128(o).unwrap_or(0).abs() as i64) - } else if let Some(r) = self.heap.foreign_unop(UnOp::Abs, o)? { - r - } else { - return Err(self.bad_operand("abs()", o)); - }; - self.push(v); Ok(()) - } - #[inline(never)] - pub fn call_int(&mut self, argc: u16, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { - let ok = core::mem::take(&mut self.job_ok); - // Two-arg form `int(string, base)` parses the string in the given radix. - if argc == 2 { - let base_v = self.pop()?; - let o = self.pop()?; - if !base_v.is_int() { return Err(cold_type("int() base must be an integer")); } - let base = base_v.as_int(); - let s = match self.heap.try_get(o) { - Some(HeapObj::Str(s)) => s.clone(), - Some(HeapObj::Bytes(b) | HeapObj::ByteArray(b)) => core::str::from_utf8(b).map_err(|_| self.invalid_int(cold_value("invalid literal"), o, base))?.into(), - _ => return Err(cold_type("int() can't convert non-string with explicit base")), - }; - let v = parse_int_radix(&mut self.heap, &s, base).map_err(|e| self.invalid_int(e, o, base))?; - self.push(v); return Ok(()); - } - if argc == 0 { self.push(Val::int(0)); return Ok(()); } - if argc > 2 { for _ in 0..argc { let _ = self.pop(); } return Err(cold_type("int() takes at most 2 arguments")); } - let o = self.pop()?; - let o = self.plain_number(o, "__int__", chunk)?; - // An int passes through unchanged. - if o.is_int() || crate::frontend::int::is_big(&self.heap, o) { self.push(o); return Ok(()); } - let v = if o.is_float() { float_to_int(&mut self.heap, o.as_float(), ftrunc)? } - else if o.is_bool() { Val::int(o.as_bool() as i64) } - else if o.is_heap() && let HeapObj::Str(s) = self.heap.get(o) { - let s = s.clone(); - parse_int_radix(&mut self.heap, &s, 10).map_err(|e| self.invalid_int(e, o, 10))? - } - else if let Some(b) = self.heap.bytes_of(o) { - let text = core::str::from_utf8(b).map(String::from).map_err(|_| cold_value("invalid literal")); - text.and_then(|t| parse_int_radix(&mut self.heap, &t, 10)).map_err(|e| self.invalid_int(e, o, 10))? - } - else if o.is_heap() && matches!(self.heap.get(o), HeapObj::Instance(..)) { - // A user `__int__` runs as a frame of the loop, its int checked as it returns. - if ok && self.check_job(o, crate::vm::jobs::Check::Int(o)) { return Ok(()); } - // Honor a user `__int__` method. - match self.try_call_dunder(o, "__int__", &[], chunk)? { - Some(r) if r.is_bool() => Val::int(r.as_bool() as i64), - Some(r) if r.is_int() || crate::frontend::int::is_big(&self.heap, r) => r, - Some(_) => return Err(cold_type("__int__ returned non-int")), - None => return Err(int_arg(self, o)), - } - } - else { return Err(int_arg(self, o)); }; - self.push(v); Ok(()) - } - /* The number in `o`, a value of a class over int or float, through the class `hook` when it defines one. */ - fn plain_number(&mut self, o: Val, hook: &str, chunk: &crate::parser::SSAChunk) -> Result { - let Some(&HeapObj::Boxed(n)) = self.heap.try_get(o) else { return Ok(o) }; - Ok(self.try_call_dunder(o, hook, &[], chunk)?.map_or(n, |r| crate::value::unboxed(r, &self.heap))) - } - /* An invalid literal error that quotes `o` and names `base` like Python, any other error as it is. */ - fn invalid_int(&self, e: VmErr, o: Val, base: i64) -> VmErr { - match e { - VmErr::Value(m) if m.contains("invalid literal") => VmErr::Raised(crate::s!("ValueError: invalid literal for int() with base ", int base, ": ", str &self.repr(o))), - e => e, - } - } - /* Converts int or parseable string to floating point. */ - #[inline(never)] - pub fn call_float(&mut self, argc: u16, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { - let ok = core::mem::take(&mut self.job_ok); - if argc == 0 { self.push(Val::float(0.0)); return Ok(()); } // `float()` is 0.0. - let o = self.pop()?; - let o = self.plain_number(o, "__float__", chunk)?; - // A float comes back as the same object. - if o.is_float() { self.push(o); return Ok(()); } - let text = match self.heap.try_get(o) { - Some(HeapObj::Str(s)) => Some(s.clone()), - _ => self.heap.bytes_of(o).map(|b| String::from_utf8_lossy(b).into_owned()), - }; - let f = if let Some(f) = num_f64(o, &self.heap) { f? } - else if let Some(s) = text { - let t = crate::util::uni::number_text(&s); - let t = t.as_ref(); - // Accept `_` digit separators, strip then parse. - let cleaned = if t.contains('_') { - if t.starts_with('_') || t.ends_with('_') || t.contains("__") { return Err(self.no_float(o)); } - t.chars().filter(|&c| c != '_').collect::() - } else { alloc::string::String::from(t) }; - cleaned.parse().map_err(|_| self.no_float(o))? - } - else if o.is_heap() && matches!(self.heap.get(o), HeapObj::Instance(..)) { - // A user `__float__` runs as a frame of the loop, its float checked as it returns. - if ok && self.check_job(o, crate::vm::jobs::Check::Float(o)) { return Ok(()); } - // Honor a user `__float__` and fall back to `__index__` like CPython. - match self.try_call_dunder(o, "__float__", &[], chunk)? { - Some(r) if r.is_float() => { self.push(r); return Ok(()); } - Some(r) => return Err(self.returned_wrong(o, "__float__", "a float", r)), - None => match self.try_call_dunder(o, "__index__", &[], chunk)? { - Some(r) if r.is_int() || r.is_bool() || crate::frontend::int::is_big(&self.heap, r) => num_f64(r, &self.heap).unwrap_or(Ok(0.0))?, - Some(r) => return Err(self.returned_wrong(o, "__index__", "an int", r)), - None => return Err(float_arg(self, o)), - }, - } - } - else { return Err(float_arg(self, o)); }; - self.push(Val::float(f)); Ok(()) - } - /* `complex(real, imag)` from numbers or one string, a value whose class defines `__complex__` asked first. */ - pub fn call_complex(&mut self, argc: u16, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { - use crate::frontend::complex::{Complex, parse, parts, val}; - let ok = core::mem::take(&mut self.job_ok); - let args = self.pop_n(argc as usize)?; - // Called where a frame may open, the dunder that converts an instance runs as a frame of the loop. - if ok && argc == 1 && let Some(&a) = args.first() && self.is_instance(a) { - if let Some(v) = self.calls_run(alloc::boxed::Box::new(ComplexOf { v: a, hook: 0 }), true)? { self.push(v); } - return Ok(()); - } - let num = |vm: &mut Self, v: Val, which: &str| -> Result { - if let Some(z) = parts(&vm.heap, crate::value::unboxed(v, &vm.heap)) { return Ok(z); } - for hook in ["__complex__", "__float__", "__index__"] { - if let Some(r) = vm.try_call_dunder(v, hook, &[], chunk)? && let Some(z) = parts(&vm.heap, r) { return Ok(z); } - } - Err(VmErr::TypeMsg(match which { - "" => crate::s!("complex() argument must be a string or a number, not ", str vm.type_name(v)), - part => crate::s!("complex() argument '", str part, "' must be a real number, not ", str vm.type_name(v)), - })) - }; - let text = |vm: &Self, v: Val| match vm.heap.try_get(v) { Some(HeapObj::Str(s)) => Some(s.clone()), _ => None }; - let z = match args.as_slice() { - [] => Complex { re: 0.0, im: 0.0 }, - [s] if text(self, *s).is_some() => parse(&text(self, *s).unwrap_or_default()).ok_or_else(|| cold_value("complex() arg is a malformed string"))?, - [a] => num(self, *a, "")?, - [a, b] => { - let (x, y) = (num(self, *a, "real")?, num(self, *b, "imag")?); - Complex { re: x.re - y.im, im: x.im + y.re } - } - _ => return Err(cold_type("complex() takes at most 2 arguments")), - }; - let v = val(&mut self.heap, z.re, z.im)?; - self.push(v); - Ok(()) - } - fn no_float(&self, o: Val) -> VmErr { - VmErr::Raised(crate::s!("ValueError: could not convert string to float: ", str &self.repr(o))) - } - pub fn call_chr(&mut self) -> Result<(), VmErr> { - let o = self.pop()?; - if !(o.is_int() || o.is_bool() || crate::frontend::int::is_big(&self.heap, o)) { return Err(VmErr::TypeMsg(crate::s!("'", str self.type_name(o), "' object cannot be interpreted as an integer"))); } - let c = self.as_i128(o).and_then(|i| u32::try_from(i).ok()).and_then(char::from_u32).ok_or_else(|| cold_value("chr() arg not in range(0x110000)"))?; - let mut s = String::with_capacity(4); - s.push(c); - self.alloc_and_push_str(s) - } - pub fn call_ord(&mut self) -> Result<(), VmErr> { - let o = self.pop()?; - if o.is_heap() - && let HeapObj::Str(s) = self.heap.get(o) { - let mut cs = s.chars(); - if let (Some(c), None) = (cs.next(), cs.next()) { - self.push(Val::int(c as i64)); return Ok(()); - } - } - // ord() of a one-byte bytes returns the byte value. - if let Some(b) = self.heap.bytes_of(o) && b.len() == 1 { - let v = b[0] as i64; - self.push(Val::int(v)); return Ok(()); - } - let found = match self.heap.try_get(o) { - Some(HeapObj::Str(s)) => s.chars().count(), - _ => match self.heap.bytes_of(o) { Some(b) => b.len(), None => return Err(VmErr::TypeMsg(crate::s!("ord() expected string of length 1, but ", str self.type_name(o), " found"))) }, - }; - Err(VmErr::TypeMsg(crate::s!("ord() expected a character, but string of length ", int found as i64, " found"))) - } - #[inline(never)] - pub fn call_round(&mut self, op: u16) -> Result<(), VmErr> { - let args = self.pop_n(op as usize)?; - let v = match (args.first().copied(), args.get(1).copied()) { - (Some(o), Some(n)) if o.is_float() && n.is_int() => { - let x = o.as_float(); - let nd = n.as_int(); - if !x.is_finite() { - Val::float(x) - } else if nd >= 0 { - // Correctly-rounded decimal (round-half-even on the true value), avoids the double-rounding `x*10^n` would introduce (e.g. round(2.675, 2) -> 2.67). - let s = alloc::format!("{:.*}", (nd as usize).min(323), x); - Val::float(s.parse().unwrap_or(x)) - } else { - let r = round_float_tens(x, nd.unsigned_abs().min(309) as usize); - if r.is_infinite() { return Err(VmErr::Raised(String::from("OverflowError: rounded value too large to represent"))); } - Val::float(r) - } - } - (Some(o), None) if o.is_float() => { - // 1-arg round returns an int, promote via int_to_val so large results don't wrap. - float_to_int(&mut self.heap, o.as_float(), fround)? - } - // Ints/bools round to themselves, negative ndigits round to tens/hundreds. - (Some(o), n) if o.is_bool() || o.is_int() || crate::frontend::int::is_big(&self.heap, o) => { - let o = if o.is_bool() { Val::int(o.as_bool() as i64) } else { o }; - match n { - // An inline int and at most 18 dropped digits stay in i64, half to even. - Some(n) if o.is_int() && (-18..0).contains(&n.as_int()) => { - let (x, f) = (o.as_int(), 10i64.pow(n.as_int().unsigned_abs() as u32)); - let (q, r) = (x.div_euclid(f), x.rem_euclid(f)); - self.heap.int(((q + (2 * r > f || (2 * r == f && q % 2 != 0)) as i64) * f) as i128)? - } - Some(n) if n.is_int() && n.as_int() < 0 => crate::frontend::int::round_tens(&mut self.heap, o, n.as_int().unsigned_abs()).unwrap_or(Ok(o))?, - _ => o, - } - } - _ => return Err(cold_type("round() requires a number")), - }; - self.push(v); Ok(()) - } - pub fn call_min(&mut self, op: u16, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { self.call_minmax(op, true, chunk) } - pub fn call_max(&mut self, op: u16, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { self.call_minmax(op, false, chunk) } - - fn call_minmax(&mut self, op: u16, is_min: bool, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { - let ok = core::mem::take(&mut self.job_ok); - let (positional, kw_flat) = self.parse_call_args(op)?; - // Optional `default=` (returned when a single iterable is empty) and `key=` (compare by key(x)). - let mut default: Option = None; - let mut key: Option = None; - for pair in kw_flat.as_chunks::<2>().0 { - match self.kw_name(pair[0]) { - Some("default") => default = Some(pair[1]), - Some("key") => { if !pair[1].is_none() { key = Some(pair[1]); } } - _ => return Err(cold_type("min()/max() got an unexpected keyword argument")), - } - } - let label = if is_min { "min() iterable argument is empty" } else { "max() iterable argument is empty" }; - // An `eval` or `exec` key runs each text as a floor of the loop. - if ok && let Some(k) = key && self.runs_text(k) { - // An iterable that may run Python has its items gathered by a job of the loop first, the call run again on them. - if let [o] = positional[..] && matches!(self.heap.try_get(o), Some(HeapObj::Instance(..) | HeapObj::Coroutine(..) | HeapObj::Iter(..))) { - let base = self.stack.len(); - self.push(o); - for &v in &kw_flat { self.push(v); } - self.job_ok = true; - let id = if is_min { NativeFnId::Min } else { NativeFnId::Max }; - if self.gather_arg(id, op, base, chunk)? { return Ok(()); } - let o = self.stack[base]; - self.stack.truncate(base); - self.job_ok = false; - let items = self.extract_iter(o)?; - if items.is_empty() { return match default { Some(d) => { self.push(d); Ok(()) }, None => Err(cold_value(label)) }; } - return self.keys_floor(k, items, is_min); - } - let items = match positional[..] { [o] => self.extract_iter(o)?, _ => positional }; - if items.is_empty() { return match default { Some(d) => { self.push(d); Ok(()) }, None => Err(cold_value(label)) }; } - return self.keys_floor(k, items, is_min); - } - // A builtin key stepping a generator among the items in Rust runs through a function of the loop, as a job stepping it, any iterable's items unknown yet. - if ok && let Some(k) = key && self.steps_whole(k) { - let leaf = |vm: &Self, vs: &[Val]| vs.iter().any(|&v| vm.steps_py(v)); - let held = match positional[..] { - [o] => match self.heap.try_get(o) { Some(HeapObj::List(rc)) => { let l = rc.borrow().clone(); leaf(self, &l) }, Some(HeapObj::Tuple(t)) => leaf(self, t), _ => true }, - _ => leaf(self, &positional), - }; - if held { key = self.through_loop(k, 1)?.or(key); } - // Values whose key now runs as frames fold as one iterable does. - if held && positional.len() > 1 && let Some(k) = key { - let all = self.heap.alloc(HeapObj::Tuple(positional))?; - return self.with_roots([all], |vm| vm.fold_run(all, crate::vm::jobs::Takes::Keyed(is_min, k), &[], ok, chunk)); - } - } - // One iterable folds, an item at a time, so a range never becomes a list. - if let [o] = positional[..] { - let takes = match key { Some(k) => crate::vm::jobs::Takes::Keyed(is_min, k), None => crate::vm::jobs::Takes::Extreme(is_min) }; - return self.fold_run(o, takes, default.as_slice(), ok, chunk); - } - // Many args are values. - let items = positional; - if items.is_empty() { - return match default { Some(d) => { self.push(d); Ok(()) }, None => Err(cold_value(label)) }; - } - // Values whose comparison or key may run user code fold as one iterable does, its dunders frames of the loop. - if self.heap.users && (items.iter().any(|v| v.is_heap()) || key.is_some_and(|k| self.heap.user(k))) { - let takes = match key { Some(k) => crate::vm::jobs::Takes::Keyed(is_min, k), None => crate::vm::jobs::Takes::Extreme(is_min) }; - let all = self.heap.alloc(HeapObj::Tuple(items))?; - return self.with_roots([all], |vm| vm.fold_run(all, takes, &[], ok, chunk)); - } - // Without a key, compare elements directly, with one, compare key(x) but return the winning element. - let keys = match key { - Some(k) => Some(self.call_rows(k, core::slice::from_ref(&items), items.len(), chunk)?), - None => None, - }; - let keys = keys.as_deref().unwrap_or(&items); - // An instance key runs `__lt__` like `sorted`, so then the operands stay rooted. - let roots = if self.any_instance(keys) { items.len() + keys.len() } else { 0 }; - let best = self.with_roots(items.iter().chain(keys).copied().take(roots), |vm| vm.extreme_index(keys, is_min, chunk))?; - self.push(items[best]); Ok(()) - } - /* Whether `item < best` for min or `item > best` for max, raising with the operator that ran. */ - #[inline] - pub(crate) fn beats(&mut self, item: Val, best: Val, is_min: bool, chunk: &crate::parser::SSAChunk) -> Result { - if item.is_int() && best.is_int() { return Ok(if is_min { item.as_int() < best.as_int() } else { item.as_int() > best.as_int() }); } - if is_min { return self.sort_lt(item, best, chunk); } - if let Some(r) = self.try_compare_dunder(crate::parser::OpCode::Gt, item, best, chunk)? { return Ok(self.truthy(r)); } - match self.values_lt(best, item, chunk) { - Err(VmErr::TypeMsg(_)) => Err(VmErr::TypeMsg(crate::s!("'>' not supported between instances of '", str self.type_name(item), "' and '", str self.type_name(best), "'"))), - r => r, - } - } - /* Index of the least key, or with `!is_min` the greatest, the first one on a tie. */ - pub(crate) fn extreme_index(&mut self, keys: &[Val], is_min: bool, chunk: &crate::parser::SSAChunk) -> Result { - let mut best = 0; - for i in 1..keys.len() { - if self.beats(keys[i], keys[best], is_min, chunk)? { best = i; } - } - Ok(best) - } - pub fn call_sum(&mut self, op: u16, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { - let ok = core::mem::take(&mut self.job_ok); - let args = self.pop_n(op as usize)?; - if args.is_empty() { return Err(cold_type("sum() requires at least 1 argument")); } - // Text and bytes join, which sum points to. - match args.get(1).and_then(|&start| self.heap.try_get(start)) { - Some(HeapObj::Str(_)) => return Err(cold_type("sum() can't sum strings [use ''.join(seq) instead]")), - Some(HeapObj::Bytes(_)) => return Err(cold_type("sum() can't sum bytes [use b''.join(seq) instead]")), - Some(HeapObj::ByteArray(_)) => return Err(cold_type("sum() can't sum bytearray [use b''.join(seq) instead]")), - _ => {} - } - // Each item adds as it comes, as CPython 3.15 adds it. - self.fold_run(args[0], crate::vm::jobs::Takes::Sum, &args[1..], ok, chunk) - } - /* One step of `sum`, through a user `__add__` or `__radd__` when either side defines one. */ - pub(crate) fn sum_add(&mut self, a: Val, b: Val, chunk: &crate::parser::SSAChunk) -> Result { - match self.try_binary_dunder(crate::parser::types::OpCode::Add, a, b, false, chunk)? { - Some(v) => Ok(v), - None => self.add_vals(crate::value::unboxed(a, &self.heap), crate::value::unboxed(b, &self.heap)), - } - } - #[inline(never)] - pub fn call_range(&mut self, op: u16) -> Result<(), VmErr> { - let args = self.pop_n(op as usize)?; - // A bound past i64 makes a wide range. - let (s, e, st) = match args[..] { [e] => (Val::int(0), e, Val::int(1)), [s, e] => (s, e, Val::int(1)), [s, e, st] => (s, e, st), _ => return Err(cold_type("range() takes 1 to 3 arguments")) }; - if let Some(r) = crate::frontend::int::new_range(&mut self.heap, s, e, st) { let r = r?; self.push(r); return Ok(()); } - // Accept any integer, bools included, that fits the i64 range bounds. - // The first bound that is no integer names its type. - if let Some(&bad) = args.iter().find(|&&a| self.as_i128(a).is_none() || a.is_float()) { - return Err(VmErr::TypeMsg(crate::s!("'", str self.type_name(bad), "' object cannot be interpreted as an integer"))); - } - let gi = |i: Option| -> Result { - match i { - Some(n) if (i64::MIN as i128..=i64::MAX as i128).contains(&n) => Ok(n as i64), - Some(_) => Err(cold_overflow()), - None => Err(cold_type("range() arguments must be integers")), - } - }; - let (s, e, st) = match args.len() { - 1 => (0, gi(self.as_i128(args[0]))?, 1), - 2 => (gi(self.as_i128(args[0]))?, gi(self.as_i128(args[1]))?, 1), - 3 => (gi(self.as_i128(args[0]))?, gi(self.as_i128(args[1]))?, gi(self.as_i128(args[2]))?), - _ => return Err(cold_type("range() takes 1 to 3 arguments")), - }; - if st == 0 { return Err(cold_value("range() arg 3 must not be zero")); } - let val = self.heap.alloc(HeapObj::Range(s, e, st))?; - self.push(val); Ok(()) - } + // Number formatting. - pub fn call_bin(&mut self) -> Result<(), VmErr> { self.call_radix(2, "0b") } - pub fn call_oct(&mut self) -> Result<(), VmErr> { self.call_radix(8, "0o") } - pub fn call_hex(&mut self) -> Result<(), VmErr> { self.call_radix(16, "0x") } - /* Push int as "" with optional sign. */ - fn call_radix(&mut self, radix: u32, prefix: &str) -> Result<(), VmErr> { - let o = self.pop()?; - let Some(d) = crate::frontend::int::radix_digits(&self.heap, o, radix) else { return Err(cold_type("integer required")); }; - let (neg, digits) = d?; - self.alloc_and_push_str(crate::s!(str if neg { "-" } else { "" }, str prefix, str &digits)) - } - pub fn call_divmod(&mut self) -> Result<(), VmErr> { - let b = self.pop()?; - let a = self.pop()?; - let q = self.floor_op(BinOp::FloorDiv, a, b, "divmod()")?; - let r = self.floor_op(BinOp::Mod, a, b, "divmod()")?; - self.alloc_and_push_tuple(alloc::vec![q, r]) - } - #[inline(never)] - pub fn call_pow(&mut self, op: u16) -> Result<(), VmErr> { - let args = self.pop_n(op as usize)?; - match args.len() { - 2 => { - let r = self.pow_vals(args[0], args[1], "pow() requires numeric operands")?; - self.push(r); - Ok(()) - } - 3 => { - // A modulus under 2^63 keeps every product in i128, so small operands skip the boxed ints. - if let (Some(b), Some(e), Some(m)) = (self.as_i128(args[0]), self.as_i128(args[1]), self.as_i128(args[2])) - && m != 0 && m.unsigned_abs() < 1 << 63 { - let (am, mut acc, mut b, mut e) = (m.abs(), 1i128, b, e); - b = b.rem_euclid(am); - // A negative exponent raises the inverse of the base, which needs it coprime to the modulus. - if e < 0 { - let (mut r, mut r1, mut t, mut t1) = (am, b, 0i128, 1i128); - while r1 != 0 { let q = r / r1; (r, r1, t, t1) = (r1, r - q * r1, t1, t - q * t1); } - if r != 1 { return Err(cold_value("base is not invertible for the given modulus")); } - (b, e) = (t.rem_euclid(am), -e); - } - while e > 0 { if e & 1 == 1 { acc = acc * b % am; } b = b * b % am; e >>= 1; } - let r = acc % am; - let r = self.heap.int(if m < 0 && r != 0 { r - am } else { r })?; - self.push(r); - return Ok(()); - } - let r = crate::frontend::int::pow_mod(&mut self.heap, args[0], args[1], args[2]) - .ok_or_else(|| cold_type("pow() 3rd argument not allowed unless all arguments are integers"))??; - self.push(r); - Ok(()) - } - _ => Err(cold_type("pow() takes 2 or 3 arguments")), - } - } - /* Two-arg power for `pow()` and `**`, exact for ints and a float for floats or negative exponents. */ - pub(crate) fn pow_vals(&mut self, a: Val, b: Val, err_msg: &'static str) -> Result { - if let (Some(ai), Some(exp)) = (self.as_i128(a), self.as_i128(b).and_then(|e| i64::try_from(e).ok())) { - if exp >= 0 { - if let Some(r) = checked_pow(ai, exp as u64) { return self.heap.int(r); } - return self.heap.foreign_binop(BinOp::Pow, a, b)?.ok_or_else(cold_overflow); - } - if ai == 0 { return Err(VmErr::Raised(String::from("ZeroDivisionError: zero to a negative power"))); } - // Clamp instead of wrapping so a huge negative exponent underflows to 0.0, base ±1 keeps its parity. - let r = match ai { - 1 => 1.0, - -1 => if exp % 2 == 0 { 1.0 } else { -1.0 }, - _ => libm::pow(ai as f64, exp as f64), - }; - return Ok(Val::float(r)); - } - if let Some(r) = self.wide_op(BinOp::Pow, a, b)? { return Ok(r); } - let (Some(fa), Some(fb)) = (num_f64(a, &self.heap), num_f64(b, &self.heap)) else { return Err(cold_type(err_msg)); }; - let (fa, fb) = (fa?, fb?); - if fa == 0.0 && fb < 0.0 { return Err(VmErr::Raised(String::from("ZeroDivisionError: zero to a negative power"))); } - // A negative base under a fractional power has only a complex result. - if crate::value::math::complex_power(fa, fb) { return crate::frontend::complex::power(&mut self.heap, fa, fb); } - fpowf(fa, fb).map(Val::float).ok_or_else(|| VmErr::Raised(String::from("OverflowError: (34, 'Numerical result out of range')"))) - } } -/* `base ** e` by squaring, None once a step leaves i128. */ -fn checked_pow(mut base: i128, mut e: u64) -> Option { - let mut acc: i128 = 1; - while e > 0 { - if e & 1 == 1 { acc = acc.checked_mul(base)?; } - e >>= 1; - if e > 0 { base = base.checked_mul(base)?; } - } - Some(acc) -} /* The TypeError of `int(x)` for a value with no integer form. */ #[cold] -fn int_arg(vm: &VM, o: Val) -> VmErr { VmErr::TypeMsg(crate::s!("int() argument must be a string, a bytes-like object or a real number, not '", str vm.type_name(o), "'")) } +pub(crate) fn int_arg(vm: &H, o: Val) -> VmErr { VmErr::TypeMsg(crate::s!("int() argument must be a string, a bytes-like object or a real number, not '", str vm.type_name(o), "'")) } /* The TypeError of `float(x)` for a value with no float form. */ #[cold] -fn float_arg(vm: &VM, o: Val) -> VmErr { VmErr::TypeMsg(crate::s!("float() argument must be a string or a real number, not '", str vm.type_name(o), "'")) } +pub(crate) fn float_arg(vm: &H, o: Val) -> VmErr { VmErr::TypeMsg(crate::s!("float() argument must be a string or a real number, not '", str vm.type_name(o), "'")) } + /* `complex(v)` of an instance as a job, calling the first of `__complex__`, `__float__` and `__index__` its class holds as a frame, `hook` next. */ -struct ComplexOf { v: Val, hook: usize } +pub(crate) struct ComplexOf { pub(crate) v: Val, pub(crate) hook: usize } impl crate::vm::jobs::Calls for ComplexOf { fn step(&mut self, vm: &mut VM<'_>, got: Option>) -> Result { diff --git a/src/vm/globals/sequence.rs b/src/vm/globals/sequence.rs index 20f92212..33f54b59 100644 --- a/src/vm/globals/sequence.rs +++ b/src/vm/globals/sequence.rs @@ -1,73 +1,18 @@ use crate::s; -use alloc::{vec, vec::Vec}; +use alloc::vec::Vec; use super::super::VM; use super::super::types::*; -use crate::parser::{OpCode, SSAChunk}; +use crate::parser::SSAChunk; +use crate::vm::natives::Builtins; impl<'a> VM<'a> { - /* A builtin iterator named `name` over `frame`. */ - fn alloc_iterator(&mut self, frame: IterFrame, name: &'static str) -> Result { - self.heap.alloc(HeapObj::Iter(alloc::rc::Rc::new(core::cell::RefCell::new(frame)), name)) - } - /* Pushes a builtin iterator named `name` over `frame`. */ - pub(crate) fn push_iterator(&mut self, frame: IterFrame, name: &'static str) -> Result<(), VmErr> { - let it = self.alloc_iterator(frame, name)?; - self.push(it); - Ok(()) - } - /* What a dict view shows now, an items view pairing each key with its value. */ - pub(crate) fn view_items(&mut self, d: Val, kind: View) -> Result, VmErr> { - let entries: Vec<(Val, Val)> = self.heap.dict_cell(d).map_or_else(Vec::new, |m| m.borrow().iter().collect()); - match kind { - View::Keys => Ok(entries.into_iter().map(|(k, _)| k).collect()), - View::Values => Ok(entries.into_iter().map(|(_, v)| v).collect()), - View::Items => entries.into_iter().map(|(k, v)| self.heap.alloc(HeapObj::Tuple(vec![k, v]))).collect(), - } - } - /* An iterator over `o`, the one `iter(o)` gives. */ - pub(crate) fn iter_of(&mut self, o: Val, chunk: &SSAChunk) -> Result { - let ascii = self.heap.str_is_ascii(o); - if self.heap.classed(o).is_some() && let Some(it) = self.try_call_dunder(o, "__iter__", &[], chunk)? { return Ok(it); } - let name = match self.heap.try_get(o) { - Some(HeapObj::Coroutine(..)) if self.is_async_object(o) => return Err(self.not_iterable(o)), - Some(HeapObj::Iter(..) | HeapObj::Coroutine(..)) => return Ok(o), - Some(HeapObj::Instance(..)) => return self.try_call_dunder(o, "__iter__", &[], chunk)?.ok_or_else(|| self.not_iterable(o)), - Some(HeapObj::List(rc)) => { let rc = rc.clone(); return self.alloc_iterator(IterFrame::List { rc, idx: 0 }, "list_iterator"); } - Some(&HeapObj::Range(cur, end, step)) => return self.alloc_iterator(IterFrame::Range { cur, end, step }, "range_iterator"), - Some(&HeapObj::BigRange(cur, end, step)) => return self.alloc_iterator(IterFrame::BigRange { cur, end, step }, "range_iterator"), - Some(HeapObj::Dict(_) | HeapObj::FrozenDict(_)) => return self.alloc_iterator(self.dict_walk(o, View::Keys), "dict_keyiterator"), - Some(&HeapObj::DictView(d, kind)) => { - let name = match kind { View::Keys => "dict_keyiterator", View::Values => "dict_valueiterator", View::Items => "dict_itemiterator" }; - return self.alloc_iterator(self.dict_walk(d, kind), name); - } - Some(HeapObj::Tuple(_)) => "tuple_iterator", - Some(HeapObj::Str(_)) => if ascii { "str_ascii_iterator" } else { "str_iterator" }, - Some(HeapObj::Set(_) | HeapObj::FrozenSet(_)) => "set_iterator", - Some(HeapObj::Bytes(_)) => "bytes_iterator", - Some(HeapObj::ByteArray(_)) => "bytearray_iterator", - Some(HeapObj::MemoryView(_)) => "memory_iterator", - _ => return Err(self.not_iterable(o)), - }; - let items = self.extract_iter(o)?; - let frame = self.watched(o, items); - self.alloc_iterator(frame, name) - } - - /* Turns the stack values from `at` into iterators in place, each rooted while the next is made. */ - fn iters_in_place(&mut self, at: usize, chunk: &SSAChunk) -> Result<(), VmErr> { - for i in at..self.stack.len() { - let it = self.iter_of(self.stack[i], chunk)?; - self.stack[i] = it; - } - Ok(()) - } /* The next item of a builtin iterator, a generator or a `__next__` object, None once spent. */ pub(crate) fn next_of(&mut self, it: Val, chunk: &SSAChunk) -> Result, VmErr> { @@ -146,29 +91,7 @@ impl<'a> VM<'a> { if let Err(e) = self.exec_call(its.len() as u16, chunk) { return self.stopped(e, base); } self.pop().map(Some) } - IterFrame::Zip { its, strict } => { - let base = self.stack.len(); - if its.is_empty() { return Ok(None); } - if !strict { - if !self.push_next_of(&its, chunk)? { return Ok(None); } - } else { - // A strict zip ends only when every iterable ends together. - for (k, &it) in its.iter().enumerate() { - match self.next_of(it, chunk)? { - Some(v) => self.push(v), - None if k > 0 => { self.stack.truncate(base); return Err(Self::zip_uneven(k, false)); } - None => { - for (j, &rest) in its.iter().enumerate().skip(1) { - if let Some(extra) = self.next_of(rest, chunk)? { let _ = extra; return Err(Self::zip_uneven(j, true)); } - } - return Ok(None); - } - } - } - } - let items = self.stack.split_off(base); - self.heap.alloc(HeapObj::Tuple(items)).map(Some) - } + IterFrame::Zip { its, strict } => self.zip_step(&its, strict, chunk), IterFrame::Filter { f, it, .. } => { let (soft, mut first) = (core::mem::take(&mut self.step_soft), true); loop { // A driven step leaves a generator's next item to its driver to run as a frame, and one its builtin test would step in Rust. if soft && !first && !matches!(self.heap.try_get(f), Some(HeapObj::Func(..) | HeapObj::Class(..) | HeapObj::Instance(..) | HeapObj::BoundUserMethod(..))) @@ -189,14 +112,7 @@ impl<'a> VM<'a> { let item = self.pop()?; if keep { return Ok(Some(item)); } } }, - IterFrame::Enumerate { it, n } => { - let Some(item) = self.next_of(it, chunk)? else { return Ok(None) }; - self.push(item); - let next = self.int_to_val(self.as_i128(n).and_then(|i| i.checked_add(1)))?; - if let Some(frame) = frame && let IterFrame::Enumerate { n: count, .. } = &mut *frame.borrow_mut() { *count = next; } - let item = self.pop()?; - self.heap.alloc(HeapObj::Tuple(vec![n, item])).map(Some) - } + IterFrame::Enumerate { it, n } => self.enumerate_step(frame, it, n, chunk), IterFrame::Call { f, sentinel } => { self.push(f); self.exec_call(0, chunk)?; @@ -224,66 +140,9 @@ impl<'a> VM<'a> { } } - /* Pushes the next item of each iterator, false with nothing pushed once one is spent. */ - fn push_next_of(&mut self, its: &[Val], chunk: &SSAChunk) -> Result { - let base = self.stack.len(); - for &it in its { - match self.next_of(it, chunk)? { - Some(v) => self.push(v), - None => { self.stack.truncate(base); return Ok(false); } - } - } - Ok(true) - } - pub fn call_len(&mut self, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { - let ok = core::mem::take(&mut self.job_ok); - let o = self.pop()?; - // An instance's `__len__` runs as a frame of the loop. - if ok && self.is_instance(o) && self.check_job(o, crate::vm::jobs::Check::Len) { return Ok(()); } - // instance `__len__` takes precedence over built-in length rules. - if let Some(r) = self.try_call_dunder(o, "__len__", &[], chunk)? { - let n = if r.is_int() { r.as_int() as i128 } - else if let Some(i) = crate::vm::types::as_i128(r, &self.heap) { i } - else { return Err(cold_type("__len__ must return int")); }; - if n < 0 { return Err(cold_value("__len__() should return >= 0")); } - let v = self.int_to_val(Some(n))?; - self.push(v); - return Ok(()); - } - let n = self.builtin_len(o)?; - let v = self.int_to_val(Some(n))?; - self.push(v); Ok(()) - } - /* `len(o)` of a builtin container, shared with the plugin ABI. */ - pub(crate) fn builtin_len(&mut self, o: Val) -> Result { - let ascii = self.heap.str_is_ascii(o); - Ok(match self.heap.try_get(o) { - Some(HeapObj::Str(s)) => (if ascii { s.len() } else { s.chars().count() }) as i128, - Some(HeapObj::Bytes(b) | HeapObj::ByteArray(b)) => b.len() as i128, - Some(HeapObj::MemoryView(m)) => if m.released { return Err(cold_value("operation forbidden on released memoryview object")) } else { m.len as i128 }, - Some(HeapObj::List(v)) => v.borrow().len() as i128, - Some(HeapObj::Tuple(v)) => v.len() as i128, - Some(HeapObj::Dict(v) | HeapObj::FrozenDict(v)) => v.borrow().len() as i128, - Some(&HeapObj::DictView(d, _)) => return self.builtin_len(d), - Some(HeapObj::Set(v)) => v.borrow().len() as i128, - Some(HeapObj::FrozenSet(v)) => v.len() as i128, - Some(&HeapObj::Range(_, _, 0)) => return Err(cold_value("range() arg 3 must not be zero")), - Some(&HeapObj::Range(s, e, st)) => crate::vm::eq::range_len(s, e, st), - Some(HeapObj::BigRange(..)) => crate::frontend::int::range_len(&self.heap, o).unwrap_or(Ok(0))?, - _ => return Err(VmErr::TypeMsg(s!("object of type '", str self.type_name(o), "' has no len()"))), - }) - } - /* `sorted` with no key, its `__lt__` run as frames of the loop when `ok`, called where one may open. */ - fn sorted_of(&mut self, reverse: bool, ok: bool, chunk: &SSAChunk) -> Result<(), VmErr> { - let o = self.pop()?; - let items = self.extract_iter(o)?; - let mut items = if ok { match self.merge_job(items, reverse, Val::undef()) { Ok(()) => return Ok(()), Err(items) => items } } else { items }; - self.sort_by_lt(&mut items, reverse, chunk)?; - self.alloc_and_push_list(items) - } /* Whether the generator on top of the stack, or the one the instance there gives, runs as a frame of the loop, gathered into the list `sorted` sorts. */ #[cold] @@ -378,146 +237,23 @@ impl<'a> VM<'a> { Ok(order.into_iter().map(|i| items[i]).collect()) } - /* Root the operands (comparators can run GC-triggering user code), sort `keys` the way Python does through `sort_lt`, and return the index permutation. `extra_roots` keeps caller-only values (e.g. the items in a keyed sort) alive across comparisons. The first comparison error ends the sort. */ - pub(crate) fn sorted_order(&mut self, keys: &[Val], extra_roots: &[Val], reverse: bool, chunk: &SSAChunk) -> Result, VmErr> { - if let Some(order) = plain_order(keys, reverse, &self.heap) { return Ok(order); } - let roots_base = self.temp_roots.len(); - // Only an instance key runs user code that could collect. - if self.any_instance(keys) { self.temp_roots.extend(keys.iter().chain(extra_roots).copied()); } - // A descending sort reverses, sorts ascending and reverses back, so equal keys keep their order. - let mut order: Vec = if reverse { (0..keys.len()).rev().collect() } else { (0..keys.len()).collect() }; - let sorted = crate::vm::timsort::sort(&mut order, &mut |x, y| self.sort_lt(keys[x], keys[y], chunk)); - if reverse { order.reverse(); } - let sort_err = sorted.err(); - self.temp_roots.truncate(roots_base); - if let Some(e) = sort_err { return Err(e); } - Ok(order) - } - /* True when a comparison over `vals` can reach a user dunder. */ - pub(crate) fn any_instance(&self, vals: &[Val]) -> bool { - vals.iter().any(|&v| matches!(self.heap.try_get(v), Some(HeapObj::Instance(..)))) - } - - // a < b via __lt__ when either side defines it, else the built-in comparison. - #[inline] - pub(crate) fn sort_lt(&mut self, a: Val, b: Val, chunk: &SSAChunk) -> Result { - if let Some(r) = self.try_compare_dunder(OpCode::Lt, a, b, chunk)? { - return Ok(self.truthy(r)); - } - self.values_lt(a, b, chunk) - } - /* In-place sort dispatching `__lt__`, roots items since a comparison can run user code that GCs. */ - pub(crate) fn sort_by_lt(&mut self, items: &mut [Val], reverse: bool, chunk: &SSAChunk) -> Result<(), VmErr> { - let snapshot = items.to_vec(); - let order = self.sorted_order(&snapshot, &[], reverse, chunk)?; - for (dst, &src) in order.iter().enumerate() { items[dst] = snapshot[src]; } - Ok(()) - } - /* `reversed(seq)`, a list read live from its end, a range stepped backwards, other sequences read up front. */ - #[inline(never)] - pub fn call_reversed(&mut self) -> Result<(), VmErr> { - let o = self.pop()?; - let name = match self.heap.try_get(o) { - Some(HeapObj::List(rc)) => { - let (rc, idx) = (rc.clone(), rc.borrow().len()); - return self.push_iterator(IterFrame::ListRev { rc, idx }, "list_reverseiterator"); - } - Some(&HeapObj::Range(start, end, step)) => { - let n = crate::vm::eq::range_len(start, end, step); - let (last, stop) = ((start as i128) + (n - 1) * step as i128, start as i128 - step as i128); - // A range whose backward bounds leave i64 reads its items up front. - if let (true, Some(back), Ok(cur), Ok(end)) = (n > 0, step.checked_neg(), i64::try_from(last), i64::try_from(stop)) { - return self.push_iterator(IterFrame::Range { cur, end, step: back }, "range_iterator"); - } - "range_iterator" - } - Some(HeapObj::BigRange(..)) => { - let (cur, end, step) = crate::frontend::int::range_reversed(&mut self.heap, o).unwrap_or(Err(VmErr::Overflow))?; - return self.push_iterator(IterFrame::BigRange { cur, end, step }, "range_iterator"); - } - Some(HeapObj::Str(_) | HeapObj::Tuple(_) | HeapObj::Bytes(_) | HeapObj::ByteArray(_)) => "reversed", - Some(HeapObj::Dict(_) | HeapObj::FrozenDict(_) | HeapObj::DictView(_, View::Keys)) => "dict_reversekeyiterator", - Some(HeapObj::DictView(_, View::Values)) => "dict_reversevalueiterator", - Some(HeapObj::DictView(_, View::Items)) => "dict_reverseitemiterator", - _ => return Err(VmErr::TypeMsg(s!("'", str self.type_name(o), "' object is not reversible"))), - }; - let mut items = self.extract_iter(o)?; - items.reverse(); - let frame = self.watched(o, items); - self.push_iterator(frame, name) - } - pub fn call_enumerate(&mut self, op: u16, chunk: &SSAChunk) -> Result<(), VmErr> { - let (positional, kw_flat) = self.parse_call_args(op)?; - if positional.is_empty() || positional.len() > 2 { - return Err(cold_type("enumerate() takes 1 or 2 positional arguments")); - } - // `start` is positional (`enumerate(xs, 5)`) or keyword (`enumerate(xs, start=5)`), default 0. - let mut start = if positional.len() == 2 { positional[1] } else { Val::int(0) }; - for pair in kw_flat.as_chunks::<2>().0 { - match self.kw_name(pair[0]) { - Some("start") => start = pair[1], - _ => return Err(cold_type("enumerate() got an unexpected keyword argument")), - } - } - if self.as_i128(start).is_none() { return Err(cold_type("enumerate() start must be an integer")); } - let it = self.with_roots([positional[0], start], |vm| vm.iter_of(positional[0], chunk))?; - self.push_iterator(IterFrame::Enumerate { it, n: start }, "enumerate") - } - /* Pairs the items of N iterables into tuples as they are asked for, stopping at the shortest. */ - pub fn call_zip(&mut self, op: u16, chunk: &SSAChunk) -> Result<(), VmErr> { - // `strict=` is the one keyword, its pair above the iterables. - let (pos, kw) = ((op & 0xFF) as usize, (op >> 8) as usize); - let mut strict = false; - for pair in self.pop_n(2 * kw)?.chunks(2) { - if self.kw_name(pair[0]) != Some("strict") { - return Err(VmErr::TypeMsg(s!("zip() got an unexpected keyword argument '", str self.kw_name(pair[0]).unwrap_or(""), "'"))); - } - strict = self.truthy(pair[1]); - } - let at = self.stack.len().checked_sub(pos).ok_or_else(|| cold_runtime("stack underflow"))?; - self.iters_in_place(at, chunk)?; - let its = self.pop_n(pos)?; - self.push_iterator(IterFrame::Zip { its: its.into(), strict }, "zip") - } - /* The error of a strict zip whose argument `k` ran out, or ran on, against those before it. */ - #[cold] - fn zip_uneven(k: usize, longer: bool) -> VmErr { - let before = if k == 1 { alloc::string::String::from("argument 1") } else { s!("arguments 1-", int k as i64) }; - let (n, how) = (k as i64 + 1, if longer { " is longer than " } else { " is shorter than " }); - cold_value_s(s!("zip() argument ", int n, str how, str &before)) - } - // TypeError for a non-iterable operand. - pub(crate) fn not_iterable(&self, o: Val) -> VmErr { - VmErr::TypeMsg(s!("'", str self.type_name(o), "' object is not iterable")) - } - /* Steps `o` lazily when a range or list, so `all`, `any` and `sum` stop early. */ - pub(in crate::vm) fn iter_cursor(&mut self, o: Val) -> Result { - Ok(match self.heap.try_get(o) { - Some(&HeapObj::Range(cur, end, step)) => IterFrame::Range { cur, end, step }, - Some(&HeapObj::BigRange(cur, end, step)) => IterFrame::BigRange { cur, end, step }, - Some(HeapObj::List(rc)) => IterFrame::List { rc: rc.clone(), idx: 0 }, - _ => IterFrame::Seq { items: self.extract_iter(o)?.into(), idx: 0 }, - }) - } /* Vec from any iterable (dict yields keys, str yields one-char strs, bytes yields ints, a generator runs to its end). */ - pub(crate) fn extract_iter(&mut self, o: Val) -> Result, VmErr> { - if !o.is_heap() { - return Err(self.not_iterable(o)); - } + // Items of iterators, generators and TypeVarTuples and none for a builtin container. + pub(crate) fn vm_items(&mut self, o: Val) -> Result>, VmErr> { // `*Ts` of a TypeVarTuple unpacks to the one `typing.Unpack[Ts]` it stands for. if matches!(self.heap.get(o), HeapObj::TypeVar(n) if n.starts_with('*') && !n.starts_with("**")) { let origin = self.heap.alloc(HeapObj::Type("typing.Unpack".into()))?; let args = self.with_roots([origin], |vm| vm.heap.alloc(HeapObj::Tuple(alloc::vec![o])))?; - return Ok(alloc::vec![self.with_roots([origin, args], |vm| vm.heap.alloc(HeapObj::GenericAlias(origin, args)))?]); + return Ok(Some(alloc::vec![self.with_roots([origin, args], |vm| vm.heap.alloc(HeapObj::GenericAlias(origin, args)))?])); } // An iterator yields what it has left, its items rooted on the stack as steps run user code. if matches!(self.heap.get(o), HeapObj::Iter(..)) { @@ -530,7 +266,7 @@ impl<'a> VM<'a> { } let out = self.stack.split_off(base); self.pop()?; - return Ok(out); + return Ok(Some(out)); } if matches!(self.heap.get(o), HeapObj::Coroutine(..)) { if self.is_async_object(o) { return Err(self.not_iterable(o)); } @@ -546,65 +282,9 @@ impl<'a> VM<'a> { } let out = self.stack.split_off(base.min(self.stack.len())); self.pop()?; - return Ok(out); - } - if let HeapObj::DictView(d, kind) = *self.heap.get(o) { - let items = self.view_items(d, kind)?; - self.charge_steps(items.len())?; - return Ok(items); + return Ok(Some(out)); } - // A wide range steps its cursor, the count it would hold checked against the memory left. - if let Some(&HeapObj::BigRange(cur, end, step)) = self.heap.try_get(o) { - let n = crate::frontend::int::range_len(&self.heap, o).unwrap_or(Ok(0))?; - if (n as u128).saturating_mul(VAL_BYTES as u128) > self.heap.room() as u128 { return Err(cold_heap()); } - let (mut walk, mut out) = (IterFrame::BigRange { cur, end, step }, Vec::new()); - while let Some(x) = walk.next_item(&mut self.heap)? { out.push(x); } - self.charge_steps(out.len())?; - return Ok(out); - } - // Snapshot the variant out so the &self borrow ends before any allocation. - let snapshot = match self.heap.get(o) { - HeapObj::List(v) => Some(v.borrow().clone()), - HeapObj::Tuple(v) => Some(v.clone()), - HeapObj::Set(v) => Some(v.borrow().iter().cloned().collect()), - HeapObj::FrozenSet(v) => Some(v.iter().cloned().collect()), - HeapObj::Range(s, e, st) => { - let (mut cur, end, step) = (*s, *e, *st); - // Materialised length is user-controlled, cap it against the memory left. - let span = (end as i128 - cur as i128).unsigned_abs(); - let count = if step == 0 { 0 } else { span / (step as i128).unsigned_abs() }; - if count.saturating_mul(VAL_BYTES as u128) > self.heap.room() as u128 { return Err(cold_heap()); } - let mut out = Vec::new(); - if step > 0 { - while cur < end { - out.push(self.heap.int(cur as i128)?); - match cur.checked_add(step) { Some(n) => cur = n, None => break } - } - } else { - while cur > end { - out.push(self.heap.int(cur as i128)?); - match cur.checked_add(step) { Some(n) => cur = n, None => break } - } - } - Some(out) - } - HeapObj::Dict(d) | HeapObj::FrozenDict(d) => Some(d.borrow().keys().collect()), - HeapObj::Bytes(b) | HeapObj::ByteArray(b) => Some(b.iter().map(|&x| Val::int(x as i64)).collect()), - HeapObj::MemoryView(_) => Some(self.heap.octets(o).unwrap_or_default().iter().map(|&x| Val::int(x as i64)).collect()), - HeapObj::Str(_) => None, // handled below, needs heap allocation - _ => return Err(self.not_iterable(o)), - }; - if let Some(v) = snapshot { - // Cost scales with element count, charge it so repeated materialisation stays bounded. - self.charge_steps(v.len())?; - return Ok(v); - } - // Str path materialises one-char heap strings via the existing helper. - if let HeapObj::Str(s) = self.heap.get(o) { - let s = s.clone(); - return self.str_to_char_vals(&s); - } - unreachable!() + Ok(None) } /* `iter(x)`, and `iter(f, sentinel)` calling `f` as each item is asked for. */ @@ -712,44 +392,10 @@ impl<'a> VM<'a> { && self.heap.classed(o).is_none() } - /* Short-circuit truthiness scan shared by `all`/`any`, stopping at the first item whose truth is `find` and pushing it, `!find` once spent. */ - fn scan_truthy(&mut self, op: u16, find: bool, arity_err: &'static str, chunk: &SSAChunk) -> Result<(), VmErr> { - let ok = core::mem::take(&mut self.job_ok); - if op != 1 { return Err(cold_type(arity_err)); } - let o = self.pop()?; - self.fold_run(o, if find { crate::vm::jobs::Takes::Any } else { crate::vm::jobs::Takes::Every }, &[], ok, chunk) - } - pub fn call_all(&mut self, op: u16, chunk: &SSAChunk) -> Result<(), VmErr> { self.scan_truthy(op, false, "all() takes exactly 1 argument", chunk) } - pub fn call_any(&mut self, op: u16, chunk: &SSAChunk) -> Result<(), VmErr> { self.scan_truthy(op, true, "any() takes exactly 1 argument", chunk) } - // Materialise an iterable to a list, strings -> chars, ranges eager, coroutines drained. - pub fn call_list(&mut self, argc: u16, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { - let ok = core::mem::take(&mut self.job_ok); - if argc == 0 { return self.alloc_and_push_list(Vec::new()); } // `list()` is the empty list. - let o = self.pop()?; - // A generator, or a user iterator's `__next__`, called where a frame may open runs as a frame of the loop, its items gathered as they come. - if ok && self.may_job(o) && self.items_job(o, crate::vm::jobs::Takes::All(OpCode::CallList, 1), chunk)? { return Ok(()); } - // user-defined iterable wins over the built-in dispatch. - if let Some(items) = self.iter_to_vec_op(o, chunk)? { - return self.alloc_and_push_list(items); - } - let items = self.extract_iter(o)?; - self.alloc_and_push_list(items) - } - pub fn call_tuple(&mut self, argc: u16, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { - let ok = core::mem::take(&mut self.job_ok); - if argc == 0 { return self.alloc_and_push_tuple(Vec::new()); } // `tuple()` is the empty tuple. - let o = self.pop()?; - if ok && self.may_job(o) && self.items_job(o, crate::vm::jobs::Takes::All(OpCode::CallTuple, 1), chunk)? { return Ok(()); } - if let Some(items) = self.iter_to_vec_op(o, chunk)? { - return self.alloc_and_push_tuple(items); - } - let items = self.extract_iter(o)?; - self.alloc_and_push_tuple(items) - } } @@ -786,7 +432,7 @@ fn plain_cmp(a: Val, b: Val, heap: &HeapPool) -> core::cmp::Ordering { } /* Stable order of keys compared without user code, None for dunders or mixes. */ -fn plain_order(keys: &[Val], reverse: bool, heap: &HeapPool) -> Option> { +pub(crate) fn plain_order(keys: &[Val], reverse: bool, heap: &HeapPool) -> Option> { let first = plain_kind(*keys.first()?, heap, 0)?; // Mixed kinds raise or need the full protocol, which the general path gives. for &k in &keys[1..] { if plain_kind(k, heap, 0)? != first { return None; } } diff --git a/src/vm/groups.rs b/src/vm/groups.rs index 03d32f3b..643946ba 100644 --- a/src/vm/groups.rs +++ b/src/vm/groups.rs @@ -7,6 +7,7 @@ use crate::parser::SSAChunk; use crate::s; use alloc::{rc::Rc, vec::Vec}; use core::cell::RefCell; +use crate::vm::natives::Builtins; /* What a split keeps, instances of a classinfo or those a predicate accepts. */ enum Cond { Types(Val), Pred(Val) } diff --git a/src/vm/helpers.rs b/src/vm/helpers.rs index 999cfbd6..85c475fd 100644 --- a/src/vm/helpers.rs +++ b/src/vm/helpers.rs @@ -1,33 +1,33 @@ use crate::s; -use alloc::{string::{String, ToString}, vec, vec::Vec}; +use alloc::{string::String, vec, vec::Vec}; use super::VM; use super::types::*; +// The shared bodies run on the VM's own heap, stack and budget. +impl Host for VM<'_> { + #[inline(always)] fn heap(&self) -> &HeapPool { &self.heap } + #[inline(always)] fn heap_mut(&mut self) -> &mut HeapPool { &mut self.heap } + #[inline(always)] fn push(&mut self, v: Val) { self.stack.push(v); } + #[inline] fn charge(&mut self, n: usize) -> Result<(), VmErr> { self.charge_steps(n) } + #[inline(always)] fn vm_items(&mut self, v: Val) -> Result>, VmErr> { VM::vm_items(self, v) } + #[inline] fn type_name(&self, v: Val) -> &str { VM::type_name(self, v) } + fn display_other(&self, v: Val, seen: &mut Vec) -> String { self.display_vm(v, seen) } + fn repr_other(&self, v: Val, seen: &mut Vec) -> Option { self.repr_vm(v, seen) } + #[inline] fn eq_without_hash(&self, cls: Val) -> bool { self.hash_hook(cls) == Some(None) } + #[inline] fn keyed(&mut self, v: Val) -> Result<(), VmErr> { VM::keyed(self, v) } + fn key_error(&mut self, key: Val) -> VmErr { VM::key_error(self, key) } + #[inline] fn root(&mut self, v: Val) { self.temp_roots.push(v); } + #[inline] fn unroot(&mut self, n: usize) { self.temp_roots.truncate(self.temp_roots.len() - n); } +} + impl<'a> VM<'a> { - // Interned keyword name behind a Val, or None when not a string. - pub(crate) fn kw_name(&self, k: Val) -> Option<&str> { - match self.heap.try_get(k) { - Some(HeapObj::Str(s)) => Some(s.as_str()), - _ => None, - } - } /* Byte offset of the last propagating error, or None on success / before `run()`. */ pub fn error_pos(&self) -> Option { self.error_byte_pos.map(|p| p as usize) } - /* `bad operand type for : ''`, what an operator or builtin of one operand says of a value it cannot take. */ - #[cold] - #[inline(never)] - pub(crate) fn bad_operand(&self, what: &str, v: Val) -> VmErr { VmErr::TypeMsg(crate::s!("bad operand type for ", str what, ": '", str self.type_name(v), "'")) } - /* `C.__float__() must return a float, not int`, what a conversion dunder of `obj` that returned `got` raises. */ - #[cold] - #[inline(never)] - pub(crate) fn returned_wrong(&self, obj: Val, dunder: &str, want: &str, got: Val) -> VmErr { - VmErr::TypeMsg(crate::s!(str self.type_name(obj), ".", str dunder, "() must return ", str want, ", not ", str self.type_name(got))) - } /* The whole report of uncaught `e` in program `src`, its chain, then where it rose, in another text when it rose in one, then the calls it left. */ pub fn render_error(&self, e: &VmErr, src: &str, path: Option<&str>) -> String { @@ -229,27 +229,6 @@ impl<'a> VM<'a> { Ok(target) } - pub(crate) fn str_to_char_vals(&mut self, s: &str) -> Result, VmErr> { - // Per-char heap allocs scale with input, charge the budget so loops over this stay bounded. - self.charge_steps(s.len())?; - s.chars().map(|c| self.heap.alloc(HeapObj::Str(c.to_string()))).collect() - } - - /* A live walk over dict `d` showing the part `kind` names. */ - pub(crate) fn dict_walk(&self, d: Val, kind: View) -> IterFrame { - let len = self.heap.dict_cell(d).map_or(0, |m| m.borrow().len()); - IterFrame::DictWalk { of: d, idx: 0, left: len, len, kind } - } - - /* A frame over `items` read from `o`, watching a dict, dict view or set for a change of size. */ - pub(crate) fn watched(&self, o: Val, items: Vec) -> IterFrame { - let of = match self.heap.try_get(o) { - Some(HeapObj::Dict(_) | HeapObj::FrozenDict(_) | HeapObj::Set(_)) => o, - Some(&HeapObj::DictView(d, _)) => d, - _ => return IterFrame::Seq { items: items.into(), idx: 0 }, - }; - IterFrame::Watched { items: items.into(), idx: 0, of, len: crate::value::watched_len(&self.heap, of) } - } pub(crate) fn make_iter_frame(&mut self, obj: Val, chunk: &crate::parser::SSAChunk) -> Result { // A new loop's iterator may sit where a freed one `lazy_over` passed by was. diff --git a/src/vm/jobs.rs b/src/vm/jobs.rs index 03420ccf..3927da76 100644 --- a/src/vm/jobs.rs +++ b/src/vm/jobs.rs @@ -8,6 +8,7 @@ use super::lower::{self, Code}; use super::opcodes::function::Passed; use super::types::*; use crate::parser::{OpCode, SSAChunk}; +use crate::vm::natives::Builtins; /* A builtin whose callback runs as a frame of the loop, what it keeps between calls, its frame's caller carrying on only once it is done. */ pub(crate) enum Job { @@ -346,6 +347,9 @@ pub(crate) enum Takes { #[derive(Clone, Copy)] pub(crate) enum Total { Word(i64), Float(f64, f64), Any } +/* A fold stopped at an item that may run user code. */ +pub(crate) struct Folded { pub(crate) j: Gather, pub(crate) cur: Option, pub(crate) first: Option, pub(crate) lazy: bool, pub(crate) co: Val } + /* A builtin over a generator, what it takes of the yields, the items gathered, and the arguments that came after the generator. */ pub(crate) struct Gather { pub(crate) takes: Takes, pub(crate) items: Vec, pub(crate) extra: Vec, pub(crate) acc: Val, pub(crate) total: Total, pub(crate) co: Val, pub(crate) before: Val, pub(crate) best_key: Val, pub(crate) pending: Val, pub(crate) cur: Option, pub(crate) pull: Pull, native: bool, gens: bool, again: Again } @@ -2306,20 +2310,6 @@ impl<'a> VM<'a> { Some(drive) } - /* A job for the builtin `takes` stands for over `of`, a generator or a user iterator, `extra` its arguments after. */ - fn gather_of(&self, takes: Takes, extra: Vec, of: Val) -> Gather { - let mut j = Gather::new(takes, extra); - // What the items come from stays held while user code the builtin runs between them collects. - j.co = of; - if takes == Takes::Sum { - // The start the items add to, 0 unless passed. - let start = j.extra.first().copied().unwrap_or(Val::int(0)); - let word = if start.is_int() || start.is_foreign() || start.is_bool() { self.as_i128(start).and_then(|n| i64::try_from(n).ok()) } else { None }; - j.total = match word { Some(w) => Total::Word(w), None if start.is_float() => Total::Float(start.as_float(), 0.0), None => Total::Any }; - j.acc = start; - } - j - } /* Runs the builtin `takes` stands for over user iterable `o`, `__iter__` once then a plain `__next__` as frames, Ok(false) for any other `o`. */ #[inline(never)] @@ -2502,56 +2492,9 @@ impl<'a> VM<'a> { }; if taken { return Ok(()); } } - // An iterator steps as asked, so the fold stops at the item that decides it, and a container is read through its cursor. - let lazy = matches!(self.heap.try_get(o), Some(HeapObj::Iter(..) | HeapObj::Coroutine(..) | HeapObj::Instance(..))); - let (co, mut cur) = if lazy { (self.iter_of(o, chunk)?, None) } else { (o, Some(self.iter_cursor(o)?)) }; - let mut j = self.gather_of(takes, extra.to_vec(), co); - let (mut first, mut held) = (None, None); - if let Some(c) = cur.as_mut() { - // Ints of a machine word add with no look at the job. - if let (Takes::Sum, Total::Word(mut w)) = (j.takes, j.total) { - while self.budget != 0 && let Some(v) = c.next_ready(&self.heap) { - match v.is_int().then(|| w.checked_add(v.as_int())).flatten() { - Some(n) => { w = n; self.budget -= 1; } - None => { held = Some(v); break; } - } - } - j.total = Total::Word(w); - } - while first.is_none() { - let next = match held.take() { Some(v) => Some(v), None => c.next_item(&mut self.heap)? }; - match next { - None => { let r = self.gather_end(&mut j, Val::none(), chunk)?; self.push(r); return Ok(()); } - Some(v) if self.budget != 0 && !self.heap.has_work() && take_plain(&mut j, v, &self.heap) => self.budget -= 1, - // An item that is no instance, the best or total no instance either, takes no user code either. - Some(v) if !self.is_instance(v) && !self.is_instance(j.acc) && match j.takes { - // Two lists or tuples whose walk meets an instance compare through its dunders. - Takes::Extreme(_) => !matches!(self.pair_scan(v, j.acc, 0), Some(PairScan::Dunder(_))), - Takes::Sum | Takes::Any | Takes::Every => true, - Takes::Keyed(_, k) => matches!(self.heap.try_get(k), Some(HeapObj::NativeFn(_))), - _ => false, - } => { - self.charge_step()?; - match j.takes { - Takes::Sum => (j.total, j.acc) = self.sum_on(j.total, j.acc, v, chunk)?, - Takes::Extreme(is_min) => if j.acc.is_undef() || self.beats(v, j.acc, is_min, chunk)? { j.acc = v; }, - // A builtin key runs at once, the keys it gives held while any `__lt__` of theirs compares them. - Takes::Keyed(is_min, k) => { - self.push(k); - self.push(v); - self.exec_call(1, chunk)?; - let kv = self.pop()?; - let best = j.best_key; - if best.is_undef() || self.with_roots([kv, best, j.acc, v], |vm| vm.beats(kv, best, is_min, chunk))? { (j.acc, j.best_key) = (v, kv); } - } - t => if self.truthy(v) == (t == Takes::Any) { self.push(Val::bool(t == Takes::Any)); return Ok(()); }, - } - } - Some(v) => first = Some(v), - } - } - self.heap.set_budget(self.budget); - } + let mut rest = None; + self.fold_ready(o, takes, extra, chunk, &mut rest)?; + let Some(Folded { mut j, cur, first, lazy, co }) = rest else { return Ok(()) }; (j.cur, j.native, j.gens) = (cur, !frames, lazy && frames && self.holds_py(co, 0, true)); let depth = self.jobs.len(); self.jobs.push(Job::Gather(Box::new(j))); @@ -2709,9 +2652,6 @@ impl<'a> VM<'a> { } } - /* Whether `v` is an instance of a user class. */ - #[inline(always)] - pub(crate) fn is_instance(&self, v: Val) -> bool { v.is_heap() && matches!(self.heap.get(v), HeapObj::Instance(..)) } /* Whether `o` is a generator or an instance, whose items a builtin's job may take as frames of the loop. */ #[inline(always)] @@ -3005,36 +2945,7 @@ impl<'a> VM<'a> { Ok(()) } - /* The total of a `sum` that went as far as `total`, `acc` its value past the floats, once `v` adds to it. */ - fn sum_on(&mut self, total: Total, acc: Val, v: Val, chunk: &SSAChunk) -> Result<(Total, Val), VmErr> { - let word = |vm: &Self, v: Val| if v.is_int() || v.is_bool() || v.is_foreign() { vm.as_i128(v).and_then(|n| i64::try_from(n).ok()) } else { None }; - let (total, acc) = match total { - Total::Word(w) => match word(self, v).and_then(|b| w.checked_add(b)) { - Some(n) => (Total::Word(n), acc), - None => { - let a = self.int_to_val(Some(w as i128))?; - let a = self.sum_through(a, v, chunk)?; - if a.is_float() { (Total::Float(a.as_float(), 0.0), a) } else { (Total::Any, a) } - } - }, - Total::Float(s, c) if v.is_float() => { let (s, c) = super::globals::numeric::neumaier(s, c, v.as_float()); (Total::Float(s, c), acc) } - Total::Float(s, c) => match word(self, v) { - Some(b) => (Total::Float(s + b as f64, c), acc), - None => (Total::Any, self.sum_through(Val::float(super::globals::numeric::settle((s, c))), v, chunk)?), - }, - Total::Any => (Total::Any, self.sum_through(acc, v, chunk)?), - }; - Ok((total, acc)) - } - /* `a + b` for `sum`, both rooted while a user `__add__` runs. */ - pub(crate) fn sum_through(&mut self, a: Val, b: Val, chunk: &SSAChunk) -> Result { - let base = self.temp_roots.len(); - self.temp_roots.extend([a, b]); - let r = self.sum_add(a, b, chunk); - self.temp_roots.truncate(base); - r - } /* The dunders `a` and `b` call for `lname` (`rname` reflected) in `dispatch_reflected`'s order, each a plain function with its operand and the other. */ fn reflected_plan(&self, a: Val, b: Val, lname: &str, rname: &str, arith: bool) -> ([(Val, Val, Val); 2], usize) { @@ -3390,7 +3301,7 @@ impl<'a> VM<'a> { Err(VmErr::Raised(crate::s!("StopIteration: ", str &self.display(ret)))) } - /* The result of the builtin of `j` once its generator returned `ret`, its items taken out of it. */ + pub(crate) fn gather_end(&mut self, j: &mut Gather, ret: Val, chunk: &SSAChunk) -> Result { match j.takes { Takes::All(OpCode::CallList, _) => self.alloc_list(core::mem::take(&mut j.items)), @@ -3411,19 +3322,7 @@ impl<'a> VM<'a> { } self.pop() } - Takes::Any => Ok(Val::bool(false)), - Takes::Every => Ok(Val::bool(true)), - Takes::Sum => Ok(match j.total { - Total::Word(w) => return self.int_to_val(Some(w as i128)), - Total::Float(s, c) => Val::float(super::globals::numeric::settle((s, c))), - Total::Any => j.acc, - }), - Takes::Extreme(is_min) | Takes::Keyed(is_min, _) if j.acc.is_undef() => match j.extra.first() { - // A `default` given is the result of an empty iterable. - Some(&d) => Ok(d), - None => Err(crate::vm::cold_value(if is_min { "min() iterable argument is empty" } else { "max() iterable argument is empty" })), - }, - Takes::Extreme(_) | Takes::Keyed(..) => Ok(j.acc), + Takes::Any | Takes::Every | Takes::Sum | Takes::Extreme(_) | Takes::Keyed(..) => self.fold_end(j), Takes::Next(d) => self.next_end(d, ret), Takes::Contains(_, not) => Ok(Val::bool(not)), Takes::Extend(l) => Ok(l), @@ -3464,7 +3363,7 @@ impl<'a> VM<'a> { /* Takes `v` into the builtin `j` with no user code and no failure, false with nothing changed when that takes the full path. */ #[inline(always)] -fn take_plain(j: &mut Gather, v: Val, heap: &HeapPool) -> bool { +pub(crate) fn take_plain(j: &mut Gather, v: Val, heap: &HeapPool) -> bool { match j.takes { Takes::Sum => match j.total { Total::Word(w) if v.is_int() && let Some(n) = w.checked_add(v.as_int()) => j.total = Total::Word(n), diff --git a/src/vm/keys.rs b/src/vm/keys.rs index 66ac630b..f19f5d98 100644 --- a/src/vm/keys.rs +++ b/src/vm/keys.rs @@ -9,15 +9,6 @@ use alloc::vec::Vec; pub(crate) enum PairScan { Dunder(usize), Differ(usize), Deep(usize), Ended } impl<'a> VM<'a> { - /* Walks the pairs of `a` and `b` from `from` as `PairScan` says, items read as they are now, None unless both are builtin lists or both builtin tuples. */ - pub(crate) fn pair_scan(&self, a: Val, b: Val, from: usize) -> Option { - let (xs, ys): (&[Val], &[Val]) = match (self.heap.plain(a)?, self.heap.plain(b)?) { - (HeapObj::Tuple(x), HeapObj::Tuple(y)) => (x, y), - (HeapObj::List(x), HeapObj::List(y)) => return Some(scan_pairs(&x.borrow(), &y.borrow(), from, &self.heap)), - _ => return None, - }; - Some(scan_pairs(xs, ys, from, &self.heap)) - } /* `a == b`, the content compare first and the user dunders only where it cannot settle. */ #[inline] @@ -198,17 +189,7 @@ impl<'a> VM<'a> { }) } - /* A set of `items`, each one hashable. */ - pub(crate) fn valset_of(&mut self, items: &[Val]) -> Result { - for &v in items.iter().filter(|v| v.is_heap()) { self.require_element(v)?; self.keyed(v)?; } - Ok(ValSet::from_vals(items, &self.heap)) - } - /* A dict of `pairs` in order, a later equal key overwriting the value and keeping the first key. */ - pub(crate) fn dictmap_of(&mut self, pairs: Vec<(Val, Val)>) -> Result { - for &(k, _) in pairs.iter().filter(|p| p.0.is_heap()) { self.require_key(k)?; self.keyed(k)?; } - Ok(DictMap::from_pairs(pairs, &self.heap)) - } /* A dict of the key and value pairs laid flat in `flat`, as a display builds it. */ pub(crate) fn dictmap_of_flat(&mut self, flat: &[Val]) -> Result { @@ -244,7 +225,7 @@ impl<'a> VM<'a> { } /* `pair_scan` over the items `xs` and `ys`. */ -fn scan_pairs(xs: &[Val], ys: &[Val], from: usize, heap: &crate::value::HeapPool) -> PairScan { +pub(crate) fn scan_pairs(xs: &[Val], ys: &[Val], from: usize, heap: &crate::value::HeapPool) -> PairScan { for k in from..xs.len().min(ys.len()) { let (x, y) = (xs[k], ys[k]); if x.0 == y.0 { continue; } diff --git a/src/vm/memview.rs b/src/vm/memview.rs index 8c267ed7..77b6f648 100644 --- a/src/vm/memview.rs +++ b/src/vm/memview.rs @@ -6,11 +6,11 @@ use super::types::*; use super::opcodes::attr_lookup::AttrLookup; use crate::s; use alloc::vec::Vec; +pub(crate) use crate::value::host::{released}; -fn released() -> VmErr { cold_value("operation forbidden on released memoryview object") } /* Where item `i` of a view sits in the bytes beneath it, a negative `i` counting from the end. */ -fn at(m: &MemView, i: i128) -> Result { +pub(crate) fn at(m: &MemView, i: i128) -> Result { let i = if i < 0 { i + m.len as i128 } else { i }; if !(0..m.len as i128).contains(&i) { return Err(cold_index("index out of bounds on dimension 1")); } Ok((m.start as isize + i as isize * m.step) as usize) @@ -31,36 +31,11 @@ impl<'a> VM<'a> { Ok(()) } - /* The view `v` holds, refused once released. */ - pub(crate) fn view(&self, v: Val) -> Option> { - match self.heap.try_get(v)? { - HeapObj::MemoryView(m) if m.released => Some(Err(released())), - &HeapObj::MemoryView(m) => Some(Ok(m)), - _ => None, - } - } /* Whether the bytes under a view can be written, a bytearray beneath it. */ fn writable(&self, m: &MemView) -> bool { matches!(self.heap.try_get(m.obj), Some(HeapObj::ByteArray(_))) } - /* `m[i]` of a view, the byte as an int. */ - pub(crate) fn view_item(&self, m: MemView, i: Val) -> Result { - let Some(i) = self.as_i128(i).filter(|_| !i.is_float()) else { - return Err(VmErr::TypeMsg(s!("memoryview: invalid slice key"))); - }; - let at = at(&m, i)?; - let byte = self.heap.bytes_of(m.obj).and_then(|b| b.get(at)).copied().ok_or_else(|| cold_index("index out of bounds on dimension 1"))?; - Ok(Val::int(byte as i64)) - } - /* `m[a:b:c]`, a view of part of the view. */ - pub(crate) fn view_slice(&mut self, m: MemView, start: Val, stop: Val, step: Val) -> Result { - let clip = |v: Val| crate::frontend::int::clip_index(&self.heap, v); - let (s, e, st) = super::opcodes::subscript::slice_bounds(clip(start), clip(stop), clip(step), m.len as i64)?; - let len = if st > 0 { ((e - s).max(0) + st - 1) / st } else { ((s - e).max(0) - st - 1) / -st } as usize; - let start = (m.start as isize + s as isize * m.step).max(0) as usize; - self.heap.alloc(HeapObj::MemoryView(MemView { obj: m.obj, start, len, step: m.step * st as isize, released: false })) - } /* `m[i] = byte` or `m[a:b] = bytes`, written into the bytearray beneath. */ pub(crate) fn view_store(&mut self, m: MemView, idx: Val, value: Val) -> Result<(), VmErr> { diff --git a/src/vm/methods/bytearray.rs b/src/vm/methods/bytearray.rs index 42ef8fa8..af3ba653 100644 --- a/src/vm/methods/bytearray.rs +++ b/src/vm/methods/bytearray.rs @@ -31,7 +31,7 @@ pub fn extend(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { // `ba.pop(i=-1)`, the byte removed as an int. pub fn pop(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { - let i = match pos.first() { Some(v) if v.is_int() => v.as_int(), None => -1, _ => return Err(cold_type("an integer is required")) }; + let i = match pos.first() { Some(v) if v.is_int() => v.as_int(), None => -1, Some(&v) if crate::frontend::int::is_big(&vm.heap, v) => return Err(VmErr::Overflow), _ => return Err(cold_type("an integer is required")) }; let b = buf_mut(vm, recv)?; if b.is_empty() { return Err(cold_index("pop from empty bytearray")); } let at = if i < 0 { i + b.len() as i64 } else { i }; @@ -42,7 +42,7 @@ pub fn pop(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { // `ba.insert(i, x)`, clamped to the ends like `list.insert`. pub fn insert(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { - let i = if pos[0].is_int() { pos[0].as_int() } else { return Err(cold_type("an integer is required")) }; + let i = if pos[0].is_int() { pos[0].as_int() } else if crate::frontend::int::is_big(&vm.heap, pos[0]) { return Err(VmErr::Overflow) } else { return Err(cold_type("an integer is required")) }; let byte = vm.byte_value(pos[1])?; let b = buf_mut(vm, recv)?; let len = b.len() as i64; @@ -64,9 +64,9 @@ pub fn clear(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { buf_mu pub fn reverse(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { buf_mut(vm, recv)?.reverse(); vm.push(Val::none()); Ok(()) } -pub fn copy(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { +pub fn copy(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let b = recv_bytes(vm, recv)?; - let v = vm.heap.alloc(HeapObj::ByteArray(b))?; + let v = vm.heap_mut().alloc(HeapObj::ByteArray(b))?; vm.push(v); Ok(()) } diff --git a/src/vm/methods/bytes.rs b/src/vm/methods/bytes.rs index 8585dffe..135909f8 100644 --- a/src/vm/methods/bytes.rs +++ b/src/vm/methods/bytes.rs @@ -2,22 +2,22 @@ use super::prelude::*; use crate::vm::opcodes::subscript::slice_bounds; // `bytes.decode([encoding[, errors]])`. 'strict' raises on invalid UTF-8, 'ignore'/'replace' recover. -pub fn decode(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn decode(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let (encoding, errors) = super::string::codec_args(vm, pos)?; let text = crate::vm::codecs::decode(recv_bytes_ref(vm, recv)?, encoding, errors)?; - let v = vm.heap.alloc(HeapObj::Str(text))?; + let v = vm.heap_mut().alloc(HeapObj::Str(text))?; vm.push(v); Ok(()) } // `bytes.hex([sep[, bytes_per_sep]])`, lowercase hex of every byte, `sep` between groups counted from the right, or the left when negative. -pub fn hex(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn hex(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let sep = match pos.first().filter(|v| !v.is_none()) { None => None, Some(&v) => { // A str separator counts its characters, so one non-ASCII character is refused for being so. - let units: Vec = match vm.heap.try_get(v) { + let units: Vec = match vm.heap().try_get(v) { Some(HeapObj::Str(t)) => t.chars().map(|c| c as u32).collect(), - _ => match vm.heap.octets(v) { Some(b) => b.iter().map(|&c| c as u32).collect(), None => return Err(VmErr::TypeMsg(crate::s!("object of type '", str vm.type_name(v), "' has no len()"))) }, + _ => match vm.heap().octets(v) { Some(b) => b.iter().map(|&c| c as u32).collect(), None => return Err(VmErr::TypeMsg(crate::s!("object of type '", str vm.type_name(v), "' has no len()"))) }, }; if units.len() != 1 { return Err(cold_value("sep must be length 1.")); } if units[0] > 0x7F { return Err(cold_value("sep must be ASCII.")); } @@ -40,14 +40,14 @@ pub fn hex(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { } // bytes-only, strings go through `string::startswith`. -pub fn startswith(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { affix_impl(vm, recv, pos, false) } -pub fn endswith(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { affix_impl(vm, recv, pos, true) } +pub fn startswith(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { affix_impl(vm, recv, pos, false) } +pub fn endswith(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { affix_impl(vm, recv, pos, true) } /* `startswith` and `endswith`, of one affix or a tuple of them, within the optional start and end. */ -fn affix_impl(vm: &mut VM, recv: Val, pos: &[Val], end: bool) -> Result<(), VmErr> { - let affixes = match vm.heap.try_get(pos[0]) { Some(HeapObj::Tuple(items)) => items.to_vec(), _ => alloc::vec![pos[0]] }; +fn affix_impl(vm: &mut H, recv: Val, pos: &[Val], end: bool) -> Result<(), VmErr> { + let affixes = match vm.heap().try_get(pos[0]) { Some(HeapObj::Tuple(items)) => items.to_vec(), _ => alloc::vec![pos[0]] }; let buf = recv_bytes_ref(vm, recv)?; - let span = window(buf, pos)?; + let span = window(buf, pos, vm.heap())?; let mut hit = false; for affix in affixes { let Ok(p) = recv_bytes_ref(vm, affix) else { @@ -61,13 +61,13 @@ fn affix_impl(vm: &mut VM, recv: Val, pos: &[Val], end: bool) -> Result<(), VmEr } /* Shared search for find/index and their right forms, `raise` turns a miss into ValueError. */ -pub fn find(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, false, false) } -pub fn index(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, false, true) } -pub fn rfind(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, true, false) } -pub fn rindex(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, true, true) } +pub fn find(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, false, false) } +pub fn index(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, false, true) } +pub fn rfind(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, true, false) } +pub fn rindex(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, true, true) } // `bytes.fromhex(s)` classmethod, parse pairs of hex digits, skipping ASCII whitespace between pairs. -pub fn fromhex(vm: &mut VM, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn fromhex(vm: &mut H, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { let s = val_to_str(vm, pos[0], "fromhex() argument must be str or bytes-like")?; let mut out: Vec = Vec::new(); let mut hi: Option = None; @@ -82,20 +82,20 @@ pub fn fromhex(vm: &mut VM, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { give(vm, out) } -pub fn lower(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { +pub fn lower(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let mut buf = recv_bytes(vm, recv)?; buf.make_ascii_lowercase(); give(vm, buf) } -pub fn upper(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { +pub fn upper(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let mut buf = recv_bytes(vm, recv)?; buf.make_ascii_uppercase(); give(vm, buf) } // bytes strip, ASCII whitespace, or any byte in the optional argument. -fn bstrip(vm: &mut VM, recv: Val, pos: &[Val], left: bool, right: bool) -> Result<(), VmErr> { +fn bstrip(vm: &mut H, recv: Val, pos: &[Val], left: bool, right: bool) -> Result<(), VmErr> { let buf = recv_bytes_ref(vm, recv)?; let chars = match pos.first() { Some(&a) => Some(recv_bytes_ref(vm, a)?), None => None }; let strip = |b: u8| -> bool { match &chars { Some(set) => set.contains(&b), None => is_ws(b) } }; @@ -106,11 +106,11 @@ fn bstrip(vm: &mut VM, recv: Val, pos: &[Val], left: bool, right: bool) -> Resul let out = buf[s..e].to_vec(); give(vm, out) } -pub fn strip(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { bstrip(vm, recv, pos, true, true) } -pub fn lstrip(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { bstrip(vm, recv, pos, true, false) } -pub fn rstrip(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { bstrip(vm, recv, pos, false, true) } +pub fn strip(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { bstrip(vm, recv, pos, true, true) } +pub fn lstrip(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { bstrip(vm, recv, pos, true, false) } +pub fn rstrip(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { bstrip(vm, recv, pos, false, true) } -pub fn join(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn join(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let items = vm.extract_iter(pos[0]).map_err(|e| if matches!(e, VmErr::TypeMsg(_)) { cold_type("can only join an iterable of bytes") } else { e })?; let sep = recv_bytes_ref(vm, recv)?; let mut out: Vec = Vec::new(); @@ -122,7 +122,7 @@ pub fn join(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { } /* What a search looks for, bytes-like or one byte by its value. */ -fn needle(vm: &VM, v: Val) -> Result, VmErr> { +fn needle(vm: &H, v: Val) -> Result, VmErr> { if v.is_int() || v.is_bool() { return u8::try_from(index_arg(vm, v)?).map(|b| alloc::vec![b]).map_err(|_| cold_value("byte must be in range(0, 256)")); } @@ -130,8 +130,8 @@ fn needle(vm: &VM, v: Val) -> Result, VmErr> { } /* A bytes-like argument, refused the way Python names any other value. */ -fn bytes_like<'v>(vm: &'v VM, v: Val) -> Result, VmErr> { - vm.heap.octets(v).ok_or_else(|| VmErr::TypeMsg(crate::s!("a bytes-like object is required, not '", str vm.type_name(v), "'"))) +fn bytes_like<'v, H: Host>(vm: &'v H, v: Val) -> Result, VmErr> { + vm.heap().octets(v).ok_or_else(|| VmErr::TypeMsg(crate::s!("a bytes-like object is required, not '", str vm.type_name(v), "'"))) } /* Start of the last `sub` in `buf`. */ @@ -147,17 +147,17 @@ fn find_from(buf: &[u8], sub: &[u8], from: usize) -> Option { } /* `buf[start:end]` from the optional args after `sub`, clamped like a slice, None when start lies past the end or past stop. */ -fn window(buf: &[u8], pos: &[Val]) -> Result, VmErr> { - let arg = |i: usize| pos.get(i).copied().unwrap_or(Val::none()); +fn window(buf: &[u8], pos: &[Val], heap: &crate::value::HeapPool) -> Result, VmErr> { + let arg = |i: usize| pos.get(i).map_or(Val::none(), |&v| crate::frontend::int::clip_index(heap, v)); let (s, e, _) = slice_bounds(arg(1), arg(2), Val::none(), buf.len() as i64)?; - let past = pos.get(1).is_some_and(|v| v.is_int() && v.as_int() > buf.len() as i64); + let past = arg(1).is_int() && arg(1).as_int() > buf.len() as i64; Ok((!past && s <= e).then_some((s as usize, e as usize))) } -fn find_impl(vm: &mut VM, recv: Val, pos: &[Val], last: bool, raise: bool) -> Result<(), VmErr> { +fn find_impl(vm: &mut H, recv: Val, pos: &[Val], last: bool, raise: bool) -> Result<(), VmErr> { let sub = needle(vm, pos[0])?; let buf = recv_bytes_ref(vm, recv)?; - let found = window(buf, pos)?.and_then(|(s, e)| if last { rfind_in(&buf[s..e], &sub).map(|i| i + s) } else { find_from(&buf[..e], &sub, s) }); + let found = window(buf, pos, vm.heap())?.and_then(|(s, e)| if last { rfind_in(&buf[s..e], &sub).map(|i| i + s) } else { find_from(&buf[..e], &sub, s) }); let idx = match found { Some(i) => i as i64, None if raise => return Err(cold_value("subsection not found")), @@ -167,10 +167,10 @@ fn find_impl(vm: &mut VM, recv: Val, pos: &[Val], last: bool, raise: bool) -> Re Ok(()) } -pub fn count(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn count(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let sub = needle(vm, pos[0])?; let (buf, sub) = (recv_bytes_ref(vm, recv)?, &sub[..]); - let Some((s, e)) = window(buf, pos)? else { vm.push(Val::int(0)); return Ok(()) }; + let Some((s, e)) = window(buf, pos, vm.heap())? else { vm.push(Val::int(0)); return Ok(()) }; let hay = &buf[s..e]; let n = if sub.is_empty() { hay.len() + 1 } else { let (mut n, mut i) = (0, 0); @@ -182,7 +182,7 @@ pub fn count(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { } // `bytes.replace(old, new[, count])`, a result past the memory limit fails before it allocates. -pub fn replace(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn replace(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let (buf, old, new) = (recv_bytes_ref(vm, recv)?, recv_bytes_ref(vm, pos[0])?, recv_bytes_ref(vm, pos[1])?); let max = match pos.get(2) { Some(n) if n.is_int() && n.as_int() >= 0 => n.as_int() as usize, _ => usize::MAX }; let mut hits = Vec::new(); @@ -193,7 +193,7 @@ pub fn replace(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { i = j + old.len().max(1); } if new.len() > old.len() { - vm.heap.reserve(buf.len().saturating_add(hits.len().saturating_mul(new.len() - old.len())))?; + vm.heap().reserve(buf.len().saturating_add(hits.len().saturating_mul(new.len() - old.len())))?; } let mut out: Vec = Vec::with_capacity(buf.len() + hits.len() * new.len()); let mut at = 0; @@ -207,7 +207,7 @@ pub fn replace(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { } // `bytes.split([sep[, maxsplit]])`, no separator splits on runs of ASCII whitespace. -pub fn split(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn split(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let buf = recv_bytes_ref(vm, recv)?; let max = match pos.get(1) { Some(n) if n.is_int() && n.as_int() >= 0 => n.as_int() as usize, _ => usize::MAX }; let mut pieces: Vec<&[u8]> = Vec::new(); @@ -243,8 +243,8 @@ pub fn split(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { fn is_ws(b: u8) -> bool { matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c) } /* Pushes `buf` as the bytes a method gives, which a bytearray receiver gets back as a bytearray. */ -fn give(vm: &mut VM, buf: Vec) -> Result<(), VmErr> { - let v = vm.heap.alloc(HeapObj::Bytes(buf))?; +fn give(vm: &mut H, buf: Vec) -> Result<(), VmErr> { + let v = vm.heap_mut().alloc(HeapObj::Bytes(buf))?; vm.push(v); Ok(()) } @@ -252,7 +252,7 @@ fn give(vm: &mut VM, buf: Vec) -> Result<(), VmErr> { // bytes predicates, class tests need a non-empty value and case tests a cased byte. macro_rules! bytes_pred { ($($name:ident => $f:expr;)*) => { $( - pub fn $name(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { + pub fn $name(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let hit = $f(recv_bytes_ref(vm, recv)?); vm.push(Val::bool(hit)); Ok(()) @@ -277,26 +277,26 @@ fn titled(b: &[u8]) -> Vec { b.iter().map(|&c| { let out = if prev { c.to_ascii_lowercase() } else { c.to_ascii_uppercase() }; prev = c.is_ascii_alphabetic(); out }).collect() } -pub fn title(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let t = titled(recv_bytes_ref(vm, recv)?); give(vm, t) } +pub fn title(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let t = titled(recv_bytes_ref(vm, recv)?); give(vm, t) } -pub fn capitalize(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { +pub fn capitalize(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let mut buf = recv_bytes(vm, recv)?; buf.make_ascii_lowercase(); if let Some(first) = buf.first_mut() { first.make_ascii_uppercase(); } give(vm, buf) } -pub fn swapcase(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { +pub fn swapcase(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let buf = recv_bytes_ref(vm, recv)?.iter().map(|&c| if c.is_ascii_lowercase() { c.to_ascii_uppercase() } else { c.to_ascii_lowercase() }).collect(); give(vm, buf) } // `ljust`/`rjust`/`center(width[, fill])`, the fill one byte, an odd centre pad leaning left on an odd width. -fn justify(vm: &mut VM, recv: Val, pos: &[Val], name: &str, align: u8) -> Result<(), VmErr> { +fn justify(vm: &mut H, recv: Val, pos: &[Val], name: &str, align: u8) -> Result<(), VmErr> { let width = width_arg(vm, pos[0])?; let fill = match pos.get(1) { None => b' ', - Some(&f) => match vm.heap.octets(f) { + Some(&f) => match vm.heap().octets(f) { Some(b) if b.len() == 1 => b[0], Some(b) => return Err(VmErr::TypeMsg(crate::s!(str name, "(): argument 2 must be a byte string of length 1, not a ", str vm.type_name(f), " object of length ", int b.len() as i64))), None => return Err(VmErr::TypeMsg(crate::s!(str name, "() argument 2 must be a byte string of length 1, not ", str vm.type_name(f)))), @@ -308,12 +308,12 @@ fn justify(vm: &mut VM, recv: Val, pos: &[Val], name: &str, align: u8) -> Result let out = core::iter::repeat_n(fill, left).chain(buf.iter().copied()).chain(core::iter::repeat_n(fill, pad - left)).collect(); give(vm, out) } -pub fn ljust(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { justify(vm, recv, pos, "ljust", b'<') } -pub fn rjust(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { justify(vm, recv, pos, "rjust", b'>') } -pub fn center(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { justify(vm, recv, pos, "center", b'^') } +pub fn ljust(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { justify(vm, recv, pos, "ljust", b'<') } +pub fn rjust(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { justify(vm, recv, pos, "rjust", b'>') } +pub fn center(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { justify(vm, recv, pos, "center", b'^') } // `bytes.zfill(width)`, zeros after any sign up to the width. -pub fn zfill(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn zfill(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let width = width_arg(vm, pos[0])?; let buf = recv_bytes_ref(vm, recv)?; let signed = matches!(buf.first(), Some(b'+' | b'-')) as usize; @@ -322,15 +322,15 @@ pub fn zfill(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { give(vm, out) } -pub fn expandtabs(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn expandtabs(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let ts = tab_size(vm, pos)?; - let out = expand_tabs(recv_bytes_ref(vm, recv)?.iter().copied(), *b"\t\n\r ", ts, vm.heap.room())?; - vm.heap.spend(out.len()); + let out = expand_tabs(recv_bytes_ref(vm, recv)?.iter().copied(), *b"\t\n\r ", ts, vm.heap().room())?; + vm.heap().spend(out.len()); give(vm, out) } // `bytes.partition(sep)` and `rpartition`, the three parts around the first or last `sep`. -fn partition_impl(vm: &mut VM, recv: Val, pos: &[Val], last: bool) -> Result<(), VmErr> { +fn partition_impl(vm: &mut H, recv: Val, pos: &[Val], last: bool) -> Result<(), VmErr> { let sep = bytes_like(vm, pos[0])?.into_owned(); if sep.is_empty() { return Err(cold_value("empty separator")); } let buf = recv_bytes_ref(vm, recv)?; @@ -341,20 +341,20 @@ fn partition_impl(vm: &mut VM, recv: Val, pos: &[Val], last: bool) -> Result<(), None => [buf.to_vec(), Vec::new(), Vec::new()], }; let mut items = Vec::with_capacity(3); - for p in parts { items.push(vm.heap.alloc(HeapObj::Bytes(p))?); } + for p in parts { items.push(vm.heap_mut().alloc(HeapObj::Bytes(p))?); } vm.alloc_and_push_tuple(items) } -pub fn partition(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { partition_impl(vm, recv, pos, false) } -pub fn rpartition(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { partition_impl(vm, recv, pos, true) } +pub fn partition(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { partition_impl(vm, recv, pos, false) } +pub fn rpartition(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { partition_impl(vm, recv, pos, true) } -pub fn removeprefix(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn removeprefix(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let affix = bytes_like(vm, pos[0])?.into_owned(); let buf = recv_bytes_ref(vm, recv)?; let out = buf.strip_prefix(&affix[..]).unwrap_or(buf).to_vec(); give(vm, out) } -pub fn removesuffix(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn removesuffix(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let affix = bytes_like(vm, pos[0])?.into_owned(); let buf = recv_bytes_ref(vm, recv)?; let out = buf.strip_suffix(&affix[..]).unwrap_or(buf).to_vec(); @@ -362,7 +362,7 @@ pub fn removesuffix(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { } // `bytes.rsplit([sep[, maxsplit]])`, `split` counted from the right. -pub fn rsplit(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn rsplit(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let max = match pos.get(1) { Some(n) if n.is_int() && n.as_int() >= 0 => n.as_int() as usize, _ => usize::MAX }; let sep = match pos.first().filter(|v| !v.is_none()) { Some(&s) => Some(bytes_like(vm, s)?.into_owned()), None => None }; let buf = recv_bytes_ref(vm, recv)?; @@ -393,7 +393,7 @@ pub fn rsplit(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { } // `bytes.splitlines([keepends])`, broken at `\n`, `\r` and `\r\n` only. -pub fn splitlines(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn splitlines(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let keep = pos.first().is_some_and(|&v| vm.truthy(v)); let buf = recv_bytes_ref(vm, recv)?; let (mut lines, mut at) = (Vec::new(), 0); @@ -407,14 +407,14 @@ pub fn splitlines(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { } /* Pushes a list of bytes parts. */ -fn push_parts(vm: &mut VM, parts: Vec>) -> Result<(), VmErr> { +fn push_parts(vm: &mut H, parts: Vec>) -> Result<(), VmErr> { let mut items = Vec::with_capacity(parts.len()); - for p in parts { items.push(vm.heap.alloc(HeapObj::Bytes(p))?); } + for p in parts { items.push(vm.heap_mut().alloc(HeapObj::Bytes(p))?); } vm.alloc_and_push_list(items) } // `bytes.maketrans(from, to)`, the 256-byte table `translate` takes. -pub fn maketrans(vm: &mut VM, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn maketrans(vm: &mut H, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { let (from, to) = (bytes_like(vm, pos[0])?.into_owned(), bytes_like(vm, pos[1])?.into_owned()); if from.len() != to.len() { return Err(cold_value("maketrans arguments must have same length")); } let mut table: Vec = (0..=255).collect(); @@ -423,7 +423,7 @@ pub fn maketrans(vm: &mut VM, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { } // `bytes.translate(table[, delete])`, every byte through the table, or kept with None, those in `delete` dropped. -pub fn translate(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn translate(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let table = match pos.first().filter(|v| !v.is_none()) { Some(&t) => Some(bytes_like(vm, t)?.into_owned()), None => None }; if table.as_ref().is_some_and(|t| t.len() != 256) { return Err(cold_value("translation table must be 256 characters long")); } let delete = match pos.get(1) { Some(&d) => bytes_like(vm, d)?.into_owned(), None => Vec::new() }; diff --git a/src/vm/methods/dict.rs b/src/vm/methods/dict.rs index e8b531cf..0d2ffd6c 100644 --- a/src/vm/methods/dict.rs +++ b/src/vm/methods/dict.rs @@ -15,23 +15,23 @@ pub fn values(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { push_ pub fn items(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { push_view(vm, recv, View::Items) } // `view.isdisjoint(other)`, true when no item of `other` is in the view. -pub fn view_isdisjoint(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn view_isdisjoint(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let other = iter_to_vec(vm, pos[0])?; - let Some(&HeapObj::DictView(d, kind)) = vm.heap.try_get(recv) else { return Err(cold_type("isdisjoint requires a dict view")) }; + let Some(&HeapObj::DictView(d, kind)) = vm.heap().try_get(recv) else { return Err(cold_type("isdisjoint requires a dict view")) }; let mine = vm.view_items(d, kind)?; let set = vm.valset_of(&mine)?; - let disjoint = !other.iter().any(|&v| set.contains(v, &vm.heap)); + let disjoint = !other.iter().any(|&v| set.contains(v, vm.heap())); vm.push(Val::bool(disjoint)); Ok(()) } // `dict.copy()`, shallow copy, mutations don't affect the original. -pub fn frozen_copy(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { vm.push(recv); Ok(()) } +pub fn frozen_copy(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { vm.push(recv); Ok(()) } -pub fn copy(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { +pub fn copy(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let entries = dict_entries(vm, recv)?; let mut dm = DictMap::with_capacity(entries.len()); - for (k, v) in entries { dm.insert(k, v, &vm.heap); } + for (k, v) in entries { dm.insert(k, v, vm.heap()); } vm.alloc_and_push_dict(dm) } @@ -53,20 +53,20 @@ pub fn popitem(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { } // `dict.fromkeys(iterable, value=None)` classmethod, new dict mapping each key to `value`. -pub fn fromkeys(vm: &mut VM, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn fromkeys(vm: &mut H, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { let keys = vm.extract_iter(pos[0])?; for &k in &keys { vm.require_key(k)?; vm.keyed(k)?; } let value = pos.get(1).copied().unwrap_or(Val::none()); let mut dm = DictMap::with_capacity(keys.len()); - for k in keys { dm.insert(k, value, &vm.heap); } + for k in keys { dm.insert(k, value, vm.heap()); } vm.alloc_and_push_dict(dm) } -pub fn get(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn get(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { vm.keyed(pos[0])?; let default = if pos.len() == 2 { pos[1] } else { Val::none() }; - let Some(rc) = vm.heap.dict_cell(recv) else { return Err(cold_type("get: receiver is not a dict")) }; - let found = rc.borrow().get(&pos[0], &vm.heap).copied(); + let Some(rc) = vm.heap().dict_cell(recv) else { return Err(cold_type("get: receiver is not a dict")) }; + let found = rc.borrow().get(&pos[0], vm.heap()).copied(); if found.is_none() { vm.require_key(pos[0])?; } vm.push(found.unwrap_or(default)); Ok(()) } diff --git a/src/vm/methods/list.rs b/src/vm/methods/list.rs index d277b43b..f00b394a 100644 --- a/src/vm/methods/list.rs +++ b/src/vm/methods/list.rs @@ -1,25 +1,27 @@ use super::prelude::*; /* `list.index`, `count` or `remove` named `name`, `eq` matches an item, the VM passes one that runs `__eq__`. */ -pub(crate) fn search(vm: &mut VM, recv: Val, name: &str, pos: &[Val], mut eq: impl FnMut(&mut VM, Val, Val) -> Result) -> Result<(), VmErr> { - let (len, ty) = match vm.heap.try_get(recv) { +pub(crate) fn search(vm: &mut H, recv: Val, name: &str, pos: &[Val], mut eq: impl FnMut(&mut H, Val, Val) -> Result) -> Result<(), VmErr> { + let (len, ty) = match vm.heap().try_get(recv) { Some(HeapObj::List(rc)) => (rc.borrow().len(), "list"), Some(HeapObj::Tuple(t)) => (t.len(), "tuple"), _ => return Err(cold_type("method requires a list receiver")), }; - vm.heap.spend(len); + vm.heap().spend(len); let len = len as i64; // Optional start/end clamp like Python, negatives count from the end. Bools count as ints. let as_i = |v: Val| -> i64 { if v.is_bool() { v.as_bool() as i64 } else { v.as_int() } }; let norm = |v: Val| (if as_i(v) < 0 { len + as_i(v) } else { as_i(v) }).clamp(0, len) as usize; let (start, stop) = if name == "index" { - let start = pos.get(1).filter(|v| v.is_int() || v.is_bool()).map_or(0, |&v| norm(v)); - (start, pos.get(2).filter(|v| v.is_int() || v.is_bool()).map_or(len as usize, |&v| norm(v)).max(start)) + // A bound past i64 clips to the inline extreme, past either end of any list. + let bound = |k: usize| pos.get(k).map(|&v| crate::frontend::int::clip_index(vm.heap(), v)).filter(|v| v.is_int()); + let start = bound(1).map_or(0, norm); + (start, bound(2).map_or(len as usize, norm).max(start)) } else { (0, len as usize) }; let mut hits = 0; // Each item is read live, a `__eq__` that shrinks the list ends the walk early. for i in start..stop { - let Some(v) = (match vm.heap.try_get(recv) { + let Some(v) = (match vm.heap().try_get(recv) { Some(HeapObj::List(rc)) => rc.borrow().get(i).copied(), Some(HeapObj::Tuple(t)) => t.get(i).copied(), _ => None, @@ -43,15 +45,15 @@ pub(crate) fn search(vm: &mut VM, recv: Val, name: &str, pos: &[Val], mut eq: im } } -pub fn index(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { - search(vm, recv, "index", pos, |vm, a, b| Ok(eq_member(a, b, &vm.heap))) +pub fn index(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { + search(vm, recv, "index", pos, |vm, a, b| Ok(eq_member(a, b, vm.heap()))) } -pub fn count(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { - search(vm, recv, "count", pos, |vm, a, b| Ok(eq_member(a, b, &vm.heap))) +pub fn count(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { + search(vm, recv, "count", pos, |vm, a, b| Ok(eq_member(a, b, vm.heap()))) } -pub fn copy(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { +pub fn copy(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let items = list_clone(vm, recv)?; vm.alloc_and_push_list(items) } @@ -87,7 +89,7 @@ pub fn extend(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { } pub fn insert(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { - if !pos[0].is_int() { return Err(cold_type("list indices must be integers")); } + if !pos[0].is_int() { return Err(if crate::frontend::int::is_big(&vm.heap, pos[0]) { VmErr::Overflow } else { cold_type("list indices must be integers") }); } let moved = list_mut(vm, recv, "insert: receiver is not a list", |list| { let i = pos[0].as_int(); let ui = if i < 0 { @@ -108,6 +110,8 @@ pub fn remove(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { } pub fn pop(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { + // An index past i64 overflows before the list is looked at. + if pos.first().is_some_and(|&i| crate::frontend::int::is_big(&vm.heap, i)) { return Err(VmErr::Overflow); } let (popped, moved) = list_mut(vm, recv, "pop: receiver is not a list", |list| { if list.is_empty() { return Err(cold_index("pop from empty list")); } if pos.is_empty() { return Ok((list.pop().unwrap(), 0)); } diff --git a/src/vm/methods/mod.rs b/src/vm/methods/mod.rs index 96d5f196..5bf73117 100644 --- a/src/vm/methods/mod.rs +++ b/src/vm/methods/mod.rs @@ -11,7 +11,7 @@ pub mod set; pub mod slice; pub mod string; -use prelude::{VM, Val, VmErr, cold_type}; +use prelude::{VM, Val, VmErr, HeapObj, Host, cold_type}; use crate::s; use alloc::string::String; @@ -28,6 +28,11 @@ pub struct MethodDesc { pub post: Post, // Whether a call changes its receiver, what it holds or where it stands. pub mutating: bool, + // Whether the result hangs on receiver and arguments alone. + pub pure: bool, + // The same body over plain values. + #[cfg(feature = "pure")] + pub plain: Option, } /* What a call does to the result a method left, settled once for the table so a call only compares a tag. */ @@ -40,6 +45,38 @@ pub enum Post { Freeze, } +// Bytes become bytearrays and built dicts or sets freeze as post asks. +pub(crate) fn post_result(vm: &mut H, post: Post, top: Val) -> Result { + if post == Post::Freeze { + if let Some(slot) = vm.heap_mut().try_get_mut(top) { + match slot { + HeapObj::Dict(rc) => *slot = HeapObj::FrozenDict(rc.clone()), + HeapObj::Set(rc) => { let s = rc.borrow().clone(); *slot = HeapObj::FrozenSet(alloc::rc::Rc::new(s)); } + _ => {} + } + } + return Ok(top); + } + let items = match vm.heap().try_get(top) { + Some(HeapObj::Bytes(b)) => { let b = b.clone(); return vm.heap_mut().alloc(HeapObj::ByteArray(b)); } + Some(HeapObj::List(l)) => l.borrow().clone(), + Some(HeapObj::Tuple(t)) => t.clone(), + _ => return Ok(top), + }; + let mut out = alloc::vec::Vec::with_capacity(items.len()); + for x in items { + out.push(match vm.heap().try_get(x) { Some(HeapObj::Bytes(b)) => { let b = b.clone(); vm.heap_mut().alloc(HeapObj::ByteArray(b))? } _ => x }); + vm.root(*out.last().unwrap_or(&x)); + } + vm.unroot(out.len()); + match vm.heap_mut().try_get_mut(top) { + Some(HeapObj::List(l)) => *l.borrow_mut() = out, + Some(HeapObj::Tuple(t)) => *t = out, + _ => {} + } + Ok(top) +} + const fn same(a: &str, b: &str) -> bool { let (a, b) = (a.as_bytes(), b.as_bytes()); if a.len() != b.len() { return false; } @@ -83,6 +120,32 @@ const fn mutating_of(ty: &str, name: &str) -> bool { false } +impl MethodDesc { + // Why call_pure leaves the method out. + pub fn excluded(&self) -> Option<&'static str> { if self.pure { None } else { excluded_of(self.ty, self.name, self.mutating) } } +} + +// The receiver it changes or the user code and frame it runs. +const fn excluded_of(ty: &str, name: &str, mutating: bool) -> Option<&'static str> { + const FRAMES: &[&str] = &["generator", "coroutine", "async_generator", "async_generator_asend", "async_generator_athrow", "anext_awaitable", "coroutine_wrapper"]; + const fn any(names: &[&str], name: &str) -> bool { + let mut i = 0; + while i < names.len() { if same(names[i], name) { return true; } i += 1; } + false + } + if mutating { return Some("changes its receiver"); } + if any(FRAMES, ty) { return Some("steps the frame of a generator or a coroutine"); } + if same(ty, "op") || same(ty, "object") || same(ty, "super") { return Some("runs the dunder of a value, which may be user code"); } + if same(ty, "iterator") { return Some("an iterator lives in the VM that made it"); } + if same(ty, "memoryview") { return Some("a view shares the buffer of the VM that holds it"); } + if same(ty, "BaseException") || same(ty, "BaseExceptionGroup") { return Some("an exception carries its traceback and the frames that raised it"); } + if same(name, "format") || same(name, "format_map") { return Some("a field may run the __format__, __str__ or __getitem__ of a user value"); } + if same(name, "sort") { return Some("a key or an item may be user code"); } + if same(name, "keys") || same(name, "values") || same(name, "items") { return Some("a view reads the dict it came from live"); } + if same(ty, "str") && same(name, "translate") { return Some("a table may be a mapping with its own __getitem__"); } + None +} + /* How a call reaches a method, plain through the table, or with the frame for user code. */ #[derive(Clone, Copy, PartialEq, Eq)] pub enum MethodKind { @@ -114,12 +177,22 @@ impl BuiltinMethodId { // Builds `ALL_METHODS` grouped by receiver type, `min..max` is the arity (max 255 = variadic). macro_rules! methods { - ( $( $ty:literal { $( $name:tt => $func:path, $min:literal .. $max:literal );* $(;)? } )* ) => { + ( $( $ty:literal { $( $name:tt => $func:path, $min:literal .. $max:literal $($pure:ident)? $(=> $body:path)? );* $(;)? } )* ) => { &[ $( $( MethodDesc { - ty: $ty, name: $name, func: $func, + ty: $ty, name: $name, func: methods!(@f $func, $($pure)?), min_args: $min, max_args: $max, kind: methods!(@k $name), post: post_of($ty, $name), mutating: mutating_of($ty, $name), + pure: methods!(@b $($pure)? $($body)?), + #[cfg(feature = "pure")] plain: methods!(@p $func, $($pure)? $($body)?), } ),* ),* ] }; + // A pure body is generic over its host. + (@f $func:path,) => { $func }; + (@f $func:path, pure) => { |vm: &mut VM, recv: Val, pos: &[Val]| $func(vm, recv, pos) }; + (@b) => { false }; (@b pure) => { true }; (@b $body:path) => { true }; + (@p $func:path,) => { None }; + (@p $func:path, pure) => { Some(|h: &mut crate::pure::PlainHost, recv: Val, pos: &[Val]| $func(h, recv, pos)) }; + // In call_pure the dunder runs through the builtin it stands for. + (@p $func:path, $body:path) => { Some(|h: &mut crate::pure::PlainHost, recv: Val, pos: &[Val]| $body(h, recv, pos)) }; (@k "sort") => { MethodKind::Sort }; (@k "translate") => { MethodKind::Mapped }; (@k "format") => { MethodKind::Format }; (@k "format_map") => { MethodKind::Format }; (@k "index") => { MethodKind::Search }; (@k "count") => { MethodKind::Search }; (@k "remove") => { MethodKind::Search }; (@k "join") => { MethodKind::Iterates }; (@k "extend") => { MethodKind::Iterates }; (@k "update") => { MethodKind::Iterates }; @@ -133,88 +206,88 @@ macro_rules! methods { // Lookup keys on (ty, name) when compiled, so group however reads best. const METHODS: &[MethodDesc] = methods! { "str" { - "encode" => string::encode, 0..2; - "upper" => string::upper, 0..0; - "lower" => string::lower, 0..0; - "strip" => string::strip, 0..1; - "capitalize" => string::capitalize, 0..0; - "title" => string::title, 0..0; - "lstrip" => string::lstrip, 0..1; - "rstrip" => string::rstrip, 0..1; - "isdigit" => string::isdigit, 0..0; - "isalpha" => string::isalpha, 0..0; - "isalnum" => string::isalnum, 0..0; - "startswith" => string::startswith, 1..3; - "endswith" => string::endswith, 1..3; - "find" => string::find, 1..3; - "count" => string::count, 1..3; - "split" => string::split, 0..2; - "join" => string::join, 1..1; - "replace" => string::replace, 2..3; - "removeprefix" => string::removeprefix, 1..1; - "removesuffix" => string::removesuffix, 1..1; - "splitlines" => string::splitlines, 0..1; - "partition" => string::partition, 1..1; - "rpartition" => string::rpartition, 1..1; - "center" => string::center, 1..2; - "zfill" => string::zfill, 1..1; - "rsplit" => string::rsplit, 0..2; - "casefold" => string::casefold, 0..0; - "swapcase" => string::swapcase, 0..0; - "ljust" => string::ljust, 1..2; - "rjust" => string::rjust, 1..2; - "expandtabs" => string::expandtabs, 0..1; - "rfind" => string::rfind, 1..3; - "index" => string::index, 1..3; - "rindex" => string::rindex, 1..3; - "isspace" => string::isspace, 0..0; - "isupper" => string::isupper, 0..0; - "islower" => string::islower, 0..0; - "istitle" => string::istitle, 0..0; - "isascii" => string::isascii, 0..0; - "isdecimal" => string::isdecimal, 0..0; - "isnumeric" => string::isnumeric, 0..0; - "isprintable" => string::isprintable, 0..0; - "isidentifier" => string::isidentifier, 0..0; - "maketrans" => string::maketrans, 1..3; + "encode" => string::encode, 0..2 pure; + "upper" => string::upper, 0..0 pure; + "lower" => string::lower, 0..0 pure; + "strip" => string::strip, 0..1 pure; + "capitalize" => string::capitalize, 0..0 pure; + "title" => string::title, 0..0 pure; + "lstrip" => string::lstrip, 0..1 pure; + "rstrip" => string::rstrip, 0..1 pure; + "isdigit" => string::isdigit, 0..0 pure; + "isalpha" => string::isalpha, 0..0 pure; + "isalnum" => string::isalnum, 0..0 pure; + "startswith" => string::startswith, 1..3 pure; + "endswith" => string::endswith, 1..3 pure; + "find" => string::find, 1..3 pure; + "count" => string::count, 1..3 pure; + "split" => string::split, 0..2 pure; + "join" => string::join, 1..1 pure; + "replace" => string::replace, 2..3 pure; + "removeprefix" => string::removeprefix, 1..1 pure; + "removesuffix" => string::removesuffix, 1..1 pure; + "splitlines" => string::splitlines, 0..1 pure; + "partition" => string::partition, 1..1 pure; + "rpartition" => string::rpartition, 1..1 pure; + "center" => string::center, 1..2 pure; + "zfill" => string::zfill, 1..1 pure; + "rsplit" => string::rsplit, 0..2 pure; + "casefold" => string::casefold, 0..0 pure; + "swapcase" => string::swapcase, 0..0 pure; + "ljust" => string::ljust, 1..2 pure; + "rjust" => string::rjust, 1..2 pure; + "expandtabs" => string::expandtabs, 0..1 pure; + "rfind" => string::rfind, 1..3 pure; + "index" => string::index, 1..3 pure; + "rindex" => string::rindex, 1..3 pure; + "isspace" => string::isspace, 0..0 pure; + "isupper" => string::isupper, 0..0 pure; + "islower" => string::islower, 0..0 pure; + "istitle" => string::istitle, 0..0 pure; + "isascii" => string::isascii, 0..0 pure; + "isdecimal" => string::isdecimal, 0..0 pure; + "isnumeric" => string::isnumeric, 0..0 pure; + "isprintable" => string::isprintable, 0..0 pure; + "isidentifier" => string::isidentifier, 0..0 pure; + "maketrans" => string::maketrans, 1..3 pure; "translate" => string::translate, 1..1; "format" => framed, 0..255; } "bytes" { - "decode" => bytes::decode, 0..2; - "hex" => bytes::hex, 0..2; - "startswith" => bytes::startswith, 1..3; - "endswith" => bytes::endswith, 1..3; - "find" => bytes::find, 1..3; - "index" => bytes::index, 1..3; - "count" => bytes::count, 1..3; - "replace" => bytes::replace, 2..3; - "split" => bytes::split, 0..2; - "lower" => bytes::lower, 0..0; - "upper" => bytes::upper, 0..0; - "strip" => bytes::strip, 0..1; - "lstrip" => bytes::lstrip, 0..1; - "rstrip" => bytes::rstrip, 0..1; - "join" => bytes::join, 1..1; - "fromhex" => bytes::fromhex, 1..1; - "rfind" => bytes::rfind, 1..3; "rindex" => bytes::rindex, 1..3; "rsplit" => bytes::rsplit, 0..2; - "splitlines" => bytes::splitlines, 0..1; "partition" => bytes::partition, 1..1; "rpartition" => bytes::rpartition, 1..1; - "removeprefix" => bytes::removeprefix, 1..1; "removesuffix" => bytes::removesuffix, 1..1; - "center" => bytes::center, 1..2; "ljust" => bytes::ljust, 1..2; "rjust" => bytes::rjust, 1..2; "zfill" => bytes::zfill, 1..1; - "expandtabs" => bytes::expandtabs, 0..1; "capitalize" => bytes::capitalize, 0..0; "title" => bytes::title, 0..0; - "swapcase" => bytes::swapcase, 0..0; "isalnum" => bytes::isalnum, 0..0; "isalpha" => bytes::isalpha, 0..0; - "isascii" => bytes::isascii, 0..0; "isdigit" => bytes::isdigit, 0..0; "islower" => bytes::islower, 0..0; - "isspace" => bytes::isspace, 0..0; "istitle" => bytes::istitle, 0..0; "isupper" => bytes::isupper, 0..0; - "maketrans" => bytes::maketrans, 2..2; "translate" => bytes::translate, 1..2; + "decode" => bytes::decode, 0..2 pure; + "hex" => bytes::hex, 0..2 pure; + "startswith" => bytes::startswith, 1..3 pure; + "endswith" => bytes::endswith, 1..3 pure; + "find" => bytes::find, 1..3 pure; + "index" => bytes::index, 1..3 pure; + "count" => bytes::count, 1..3 pure; + "replace" => bytes::replace, 2..3 pure; + "split" => bytes::split, 0..2 pure; + "lower" => bytes::lower, 0..0 pure; + "upper" => bytes::upper, 0..0 pure; + "strip" => bytes::strip, 0..1 pure; + "lstrip" => bytes::lstrip, 0..1 pure; + "rstrip" => bytes::rstrip, 0..1 pure; + "join" => bytes::join, 1..1 pure; + "fromhex" => bytes::fromhex, 1..1 pure; + "rfind" => bytes::rfind, 1..3 pure; "rindex" => bytes::rindex, 1..3 pure; "rsplit" => bytes::rsplit, 0..2 pure; + "splitlines" => bytes::splitlines, 0..1 pure; "partition" => bytes::partition, 1..1 pure; "rpartition" => bytes::rpartition, 1..1 pure; + "removeprefix" => bytes::removeprefix, 1..1 pure; "removesuffix" => bytes::removesuffix, 1..1 pure; + "center" => bytes::center, 1..2 pure; "ljust" => bytes::ljust, 1..2 pure; "rjust" => bytes::rjust, 1..2 pure; "zfill" => bytes::zfill, 1..1 pure; + "expandtabs" => bytes::expandtabs, 0..1 pure; "capitalize" => bytes::capitalize, 0..0 pure; "title" => bytes::title, 0..0 pure; + "swapcase" => bytes::swapcase, 0..0 pure; "isalnum" => bytes::isalnum, 0..0 pure; "isalpha" => bytes::isalpha, 0..0 pure; + "isascii" => bytes::isascii, 0..0 pure; "isdigit" => bytes::isdigit, 0..0 pure; "islower" => bytes::islower, 0..0 pure; + "isspace" => bytes::isspace, 0..0 pure; "istitle" => bytes::istitle, 0..0 pure; "isupper" => bytes::isupper, 0..0 pure; + "maketrans" => bytes::maketrans, 2..2 pure; "translate" => bytes::translate, 1..2 pure; } "list" { "__init__" => list::init, 0..1; - "__getitem__" => object::container_getitem, 1..1; + "__getitem__" => object::container_getitem, 1..1 pure; "__setitem__" => object::container_setitem, 2..2; - "__len__" => object::container_len, 0..0; - "index" => list::index, 1..3; - "count" => list::count, 1..1; - "copy" => list::copy, 0..0; + "__len__" => object::container_len, 0..0 pure; + "index" => list::index, 1..3 pure; + "count" => list::count, 1..1 pure; + "copy" => list::copy, 0..0 pure; "append" => list::append, 1..1; "clear" => list::clear, 0..0; "reverse" => list::reverse, 0..0; @@ -226,31 +299,31 @@ const METHODS: &[MethodDesc] = methods! { } "dict" { "__init__" => dict::update, 0..1; - "__getitem__" => object::container_getitem, 1..1; + "__getitem__" => object::container_getitem, 1..1 pure; "__setitem__" => object::container_setitem, 2..2; - "__len__" => object::container_len, 0..0; + "__len__" => object::container_len, 0..0 pure; "keys" => dict::keys, 0..0; "values" => dict::values, 0..0; "items" => dict::items, 0..0; - "copy" => dict::copy, 0..0; + "copy" => dict::copy, 0..0 pure; "popitem" => dict::popitem, 0..0; - "get" => dict::get, 1..2; + "get" => dict::get, 1..2 pure; "update" => dict::update, 0..1; "pop" => dict::pop, 1..2; "setdefault" => dict::setdefault, 1..2; - "fromkeys" => dict::fromkeys, 1..2; + "fromkeys" => dict::fromkeys, 1..2 pure; } // A frozendict reads like a dict, `copy` gives itself back and `fromkeys` makes a frozendict. "frozendict" { "keys" => dict::keys, 0..0; "values" => dict::values, 0..0; "items" => dict::items, 0..0; - "get" => dict::get, 1..2; "copy" => dict::frozen_copy, 0..0; "fromkeys" => dict::fromkeys, 1..2; - "__getitem__" => object::container_getitem, 1..1; "__len__" => object::container_len, 0..0; + "get" => dict::get, 1..2 pure; "copy" => dict::frozen_copy, 0..0 pure; "fromkeys" => dict::fromkeys, 1..2 pure; + "__getitem__" => object::container_getitem, 1..1 pure; "__len__" => object::container_len, 0..0 pure; } // A frozenset answers the set methods that change nothing, with frozenset results. "frozenset" { - "union" => set::union, 0..255; "intersection" => set::intersection, 0..255; "difference" => set::difference, 0..255; - "symmetric_difference" => set::symmetric_difference, 1..1; "issubset" => set::issubset, 1..1; - "issuperset" => set::issuperset, 1..1; "isdisjoint" => set::isdisjoint, 1..1; "copy" => set::frozen_copy, 0..0; + "union" => set::union, 0..255 pure; "intersection" => set::intersection, 0..255 pure; "difference" => set::difference, 0..255 pure; + "symmetric_difference" => set::symmetric_difference, 1..1 pure; "issubset" => set::issubset, 1..1 pure; + "issuperset" => set::issuperset, 1..1 pure; "isdisjoint" => set::isdisjoint, 1..1 pure; "copy" => set::frozen_copy, 0..0 pure; } "set" { "add" => set::add, 1..1; @@ -259,43 +332,43 @@ const METHODS: &[MethodDesc] = methods! { "pop" => set::pop, 0..0; "clear" => set::clear, 0..0; "update" => set::update, 0..255; - "copy" => set::copy, 0..0; - "union" => set::union, 0..255; - "intersection" => set::intersection, 0..255; - "difference" => set::difference, 0..255; - "symmetric_difference" => set::symmetric_difference, 1..1; + "copy" => set::copy, 0..0 pure; + "union" => set::union, 0..255 pure; + "intersection" => set::intersection, 0..255 pure; + "difference" => set::difference, 0..255 pure; + "symmetric_difference" => set::symmetric_difference, 1..1 pure; "intersection_update" => set::intersection_update, 0..255; "difference_update" => set::difference_update, 0..255; "symmetric_difference_update" => set::symmetric_difference_update, 1..1; - "issubset" => set::issubset, 1..1; - "issuperset" => set::issuperset, 1..1; - "isdisjoint" => set::isdisjoint, 1..1; + "issubset" => set::issubset, 1..1 pure; + "issuperset" => set::issuperset, 1..1 pure; + "isdisjoint" => set::isdisjoint, 1..1 pure; } "int" { - "bit_length" => numeric::bit_length, 0..0; - "bit_count" => numeric::bit_count, 0..0; - "to_bytes" => numeric::to_bytes, 0..3; - "from_bytes" => numeric::from_bytes, 1..3; - "as_integer_ratio" => numeric::int_ratio, 0..0; - "is_integer" => numeric::int_is_integer, 0..0; + "bit_length" => numeric::bit_length, 0..0 pure; + "bit_count" => numeric::bit_count, 0..0 pure; + "to_bytes" => numeric::to_bytes, 0..3 pure; + "from_bytes" => numeric::from_bytes, 1..3 pure; + "as_integer_ratio" => numeric::int_ratio, 0..0 pure; + "is_integer" => numeric::int_is_integer, 0..0 pure; } "float" { - "is_integer" => numeric::is_integer, 0..0; - "as_integer_ratio" => numeric::float_ratio, 0..0; - "hex" => numeric::float_hex, 0..0; - "fromhex" => numeric::float_fromhex, 1..1; - "from_number" => numeric::float_from_number, 1..1; + "is_integer" => numeric::is_integer, 0..0 pure; + "as_integer_ratio" => numeric::float_ratio, 0..0 pure; + "hex" => numeric::float_hex, 0..0 pure; + "fromhex" => numeric::float_fromhex, 1..1 pure; + "from_number" => numeric::float_from_number, 1..1 pure; } // Appended groups, keep snapshot method ids stable. "dict" { "clear" => dict::clear, 0..0; } "slice" { - "indices" => slice::indices, 1..1; + "indices" => slice::indices, 1..1 pure; } "range" { - "index" => object::range_index, 1..1; - "count" => object::range_count, 1..1; + "index" => object::range_index, 1..1 pure; + "count" => object::range_count, 1..1 pure; } "BaseException" { "__init__" => object::exc_init, 0..255; @@ -306,14 +379,14 @@ const METHODS: &[MethodDesc] = methods! { "__iter__" => object::iter_self, 0..0; } "dict_keys" { - "isdisjoint" => dict::view_isdisjoint, 1..1; + "isdisjoint" => dict::view_isdisjoint, 1..1 pure; } "dict_items" { - "isdisjoint" => dict::view_isdisjoint, 1..1; + "isdisjoint" => dict::view_isdisjoint, 1..1 pure; } "tuple" { - "index" => list::index, 1..3; - "count" => list::count, 1..1; + "index" => list::index, 1..3 pure; + "count" => list::count, 1..1 pure; } "str" { "format_map" => framed, 1..1; @@ -360,23 +433,23 @@ const METHODS: &[MethodDesc] = methods! { } // A bytearray answers the bytes methods, its results bytearrays again, and the methods that change it. "bytearray" { - "decode" => bytes::decode, 0..2; "hex" => bytes::hex, 0..2; "startswith" => bytes::startswith, 1..3; - "endswith" => bytes::endswith, 1..3; "find" => bytes::find, 1..3; "index" => bytes::index, 1..3; - "count" => bytes::count, 1..3; "replace" => bytes::replace, 2..3; "split" => bytes::split, 0..2; - "lower" => bytes::lower, 0..0; "upper" => bytes::upper, 0..0; "strip" => bytes::strip, 0..1; - "lstrip" => bytes::lstrip, 0..1; "rstrip" => bytes::rstrip, 0..1; "join" => bytes::join, 1..1; - "fromhex" => bytes::fromhex, 1..1; "append" => bytearray::append, 1..1; "extend" => bytearray::extend, 1..1; + "decode" => bytes::decode, 0..2 pure; "hex" => bytes::hex, 0..2 pure; "startswith" => bytes::startswith, 1..3 pure; + "endswith" => bytes::endswith, 1..3 pure; "find" => bytes::find, 1..3 pure; "index" => bytes::index, 1..3 pure; + "count" => bytes::count, 1..3 pure; "replace" => bytes::replace, 2..3 pure; "split" => bytes::split, 0..2 pure; + "lower" => bytes::lower, 0..0 pure; "upper" => bytes::upper, 0..0 pure; "strip" => bytes::strip, 0..1 pure; + "lstrip" => bytes::lstrip, 0..1 pure; "rstrip" => bytes::rstrip, 0..1 pure; "join" => bytes::join, 1..1 pure; + "fromhex" => bytes::fromhex, 1..1 pure; "append" => bytearray::append, 1..1; "extend" => bytearray::extend, 1..1; "pop" => bytearray::pop, 0..1; "insert" => bytearray::insert, 2..2; "remove" => bytearray::remove, 1..1; - "clear" => bytearray::clear, 0..0; "reverse" => bytearray::reverse, 0..0; "copy" => bytearray::copy, 0..0; - "rfind" => bytes::rfind, 1..3; "rindex" => bytes::rindex, 1..3; "rsplit" => bytes::rsplit, 0..2; - "splitlines" => bytes::splitlines, 0..1; "partition" => bytes::partition, 1..1; "rpartition" => bytes::rpartition, 1..1; - "removeprefix" => bytes::removeprefix, 1..1; "removesuffix" => bytes::removesuffix, 1..1; - "center" => bytes::center, 1..2; "ljust" => bytes::ljust, 1..2; "rjust" => bytes::rjust, 1..2; "zfill" => bytes::zfill, 1..1; - "expandtabs" => bytes::expandtabs, 0..1; "capitalize" => bytes::capitalize, 0..0; "title" => bytes::title, 0..0; - "swapcase" => bytes::swapcase, 0..0; "isalnum" => bytes::isalnum, 0..0; "isalpha" => bytes::isalpha, 0..0; - "isascii" => bytes::isascii, 0..0; "isdigit" => bytes::isdigit, 0..0; "islower" => bytes::islower, 0..0; - "isspace" => bytes::isspace, 0..0; "istitle" => bytes::istitle, 0..0; "isupper" => bytes::isupper, 0..0; - "maketrans" => bytes::maketrans, 2..2; "translate" => bytes::translate, 1..2; + "clear" => bytearray::clear, 0..0; "reverse" => bytearray::reverse, 0..0; "copy" => bytearray::copy, 0..0 pure; + "rfind" => bytes::rfind, 1..3 pure; "rindex" => bytes::rindex, 1..3 pure; "rsplit" => bytes::rsplit, 0..2 pure; + "splitlines" => bytes::splitlines, 0..1 pure; "partition" => bytes::partition, 1..1 pure; "rpartition" => bytes::rpartition, 1..1 pure; + "removeprefix" => bytes::removeprefix, 1..1 pure; "removesuffix" => bytes::removesuffix, 1..1 pure; + "center" => bytes::center, 1..2 pure; "ljust" => bytes::ljust, 1..2 pure; "rjust" => bytes::rjust, 1..2 pure; "zfill" => bytes::zfill, 1..1 pure; + "expandtabs" => bytes::expandtabs, 0..1 pure; "capitalize" => bytes::capitalize, 0..0 pure; "title" => bytes::title, 0..0 pure; + "swapcase" => bytes::swapcase, 0..0 pure; "isalnum" => bytes::isalnum, 0..0 pure; "isalpha" => bytes::isalpha, 0..0 pure; + "isascii" => bytes::isascii, 0..0 pure; "isdigit" => bytes::isdigit, 0..0 pure; "islower" => bytes::islower, 0..0 pure; + "isspace" => bytes::isspace, 0..0 pure; "istitle" => bytes::istitle, 0..0 pure; "isupper" => bytes::isupper, 0..0 pure; + "maketrans" => bytes::maketrans, 2..2 pure; "translate" => bytes::translate, 1..2 pure; "resize" => bytearray::resize, 1..1; "take_bytes" => bytearray::take_bytes, 0..1; } "memoryview" { @@ -405,13 +478,13 @@ const METHODS: &[MethodDesc] = methods! { "__ror__" => framed, 1..1; "__xor__" => framed, 1..1; "__rxor__" => framed, 1..1; "__lshift__" => framed, 1..1; "__rlshift__" => framed, 1..1; "__rshift__" => framed, 1..1; "__rrshift__" => framed, 1..1; "__divmod__" => framed, 1..1; "__rdivmod__" => framed, 1..1; "__eq__" => framed, 1..1; "__ne__" => framed, 1..1; "__lt__" => framed, 1..1; - "__le__" => framed, 1..1; "__gt__" => framed, 1..1; "__ge__" => framed, 1..1; "__contains__" => framed, 1..1; - "__getitem__" => framed, 1..1; "__format__" => framed, 1..1; "__setitem__" => framed, 2..2; "__delitem__" => framed, 1..1; + "__le__" => framed, 1..1; "__gt__" => framed, 1..1; "__ge__" => framed, 1..1; "__contains__" => framed, 1..1 => object::op_contains; + "__getitem__" => framed, 1..1 => object::op_getitem; "__format__" => framed, 1..1; "__setitem__" => framed, 2..2; "__delitem__" => framed, 1..1; "__pow__" => framed, 1..2; "__round__" => framed, 0..1; "__neg__" => framed, 0..0; "__pos__" => framed, 0..0; "__abs__" => framed, 0..0; "__invert__" => framed, 0..0; - "__bool__" => framed, 0..0; "__int__" => framed, 0..0; "__float__" => framed, 0..0; "__index__" => framed, 0..0; - "__hash__" => framed, 0..0; "__repr__" => framed, 0..0; "__str__" => framed, 0..0; "__len__" => framed, 0..0; + "__bool__" => framed, 0..0 => object::op_bool; "__int__" => framed, 0..0; "__float__" => framed, 0..0; "__index__" => framed, 0..0; + "__hash__" => framed, 0..0; "__repr__" => framed, 0..0 => object::op_repr; "__str__" => framed, 0..0 => object::op_str; "__len__" => framed, 0..0 => object::container_len; "__iter__" => framed, 0..0; "__trunc__" => framed, 0..0; "__floor__" => framed, 0..0; "__ceil__" => framed, 0..0; "conjugate" => framed, 0..0; } diff --git a/src/vm/methods/numeric.rs b/src/vm/methods/numeric.rs index 26defa5a..58256ada 100644 --- a/src/vm/methods/numeric.rs +++ b/src/vm/methods/numeric.rs @@ -1,46 +1,46 @@ use super::prelude::*; -pub fn bit_length(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { - let (bits, _) = crate::frontend::int::bits(&vm.heap, recv).ok_or_else(|| cold_type("bit_length() requires an int"))?; +pub fn bit_length(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { + let (bits, _) = crate::frontend::int::bits(vm.heap(), recv).ok_or_else(|| cold_type("bit_length() requires an int"))?; vm.push(Val::int(bits as i64)); Ok(()) } -pub fn bit_count(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { - let (_, ones) = crate::frontend::int::bits(&vm.heap, recv).ok_or_else(|| cold_type("bit_count() requires an int"))?; +pub fn bit_count(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { + let (_, ones) = crate::frontend::int::bits(vm.heap(), recv).ok_or_else(|| cold_type("bit_count() requires an int"))?; vm.push(Val::int(ones as i64)); Ok(()) } // `int.to_bytes(length=1, byteorder='big')`, unsigned (signed=False), errors if it doesn't fit. -pub fn to_bytes(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn to_bytes(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let length = match pos.first() { Some(v) if v.is_int() => v.as_int().max(0) as usize, None => 1, _ => return Err(cold_type("length must be an integer")) }; // Length is user-controlled, cap it against the memory left so a huge count errors instead of aborting. - vm.heap.reserve(length)?; + vm.heap().reserve(length)?; let big = byteorder_is_big(vm, pos.get(1))?; let signed = pos.get(2).is_some_and(|&s| vm.truthy(s)); - let out = crate::frontend::int::to_bytes(&vm.heap, recv, length, big, signed).ok_or_else(|| cold_type("to_bytes() requires an int"))??; - let val = vm.heap.alloc(HeapObj::Bytes(out))?; + let out = crate::frontend::int::to_bytes(vm.heap(), recv, length, big, signed).ok_or_else(|| cold_type("to_bytes() requires an int"))??; + let val = vm.heap_mut().alloc(HeapObj::Bytes(out))?; vm.push(val); Ok(()) } // `int.from_bytes(bytes, byteorder='big')` classmethod, unsigned. -pub fn from_bytes(vm: &mut VM, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn from_bytes(vm: &mut H, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { let buf = recv_bytes_ref(vm, pos[0])?.to_vec(); let big = byteorder_is_big(vm, pos.get(1))?; let signed = pos.get(2).is_some_and(|&s| vm.truthy(s)); - let v = crate::frontend::int::from_bytes(&mut vm.heap, &buf, big, signed)?; + let v = crate::frontend::int::from_bytes(vm.heap_mut(), &buf, big, signed)?; vm.push(v); Ok(()) } -pub fn is_integer(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { +pub fn is_integer(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let f = if recv.is_float() { recv.as_float() } else { return Err(cold_type("is_integer() requires a float")); }; vm.push(Val::bool(f.is_finite() && f == libm::trunc(f))); Ok(()) } -fn byteorder_is_big(vm: &VM, arg: Option<&Val>) -> Result { +fn byteorder_is_big(vm: &H, arg: Option<&Val>) -> Result { match arg { None => Ok(true), // default is 'big' Some(&v) if v.is_none() => Ok(true), - Some(&v) => match vm.heap.try_get(v) { + Some(&v) => match vm.heap().try_get(v) { Some(HeapObj::Str(s)) if s == "big" => Ok(true), Some(HeapObj::Str(s)) if s == "little" => Ok(false), _ => Err(cold_value("byteorder must be 'little' or 'big'")), @@ -49,23 +49,23 @@ fn byteorder_is_big(vm: &VM, arg: Option<&Val>) -> Result { } /* `n.as_integer_ratio()` of an int, the int itself over one. */ -pub fn int_ratio(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { - let n = crate::value::unboxed(recv, &vm.heap); +pub fn int_ratio(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { + let n = crate::value::unboxed(recv, vm.heap()); let n = if n.is_bool() { Val::int(n.as_bool() as i64) } else { n }; vm.alloc_and_push_tuple(alloc::vec![n, Val::int(1)]) } /* `n.is_integer()` of an int, always true. */ -pub fn int_is_integer(vm: &mut VM, _recv: Val, _pos: &[Val]) -> Result<(), VmErr> { vm.push(Val::bool(true)); Ok(()) } +pub fn int_is_integer(vm: &mut H, _recv: Val, _pos: &[Val]) -> Result<(), VmErr> { vm.push(Val::bool(true)); Ok(()) } /* `x.as_integer_ratio()` of a float, the exact fraction in lowest terms with a power of two below. */ -pub fn float_ratio(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { - let f = crate::value::unboxed(recv, &vm.heap).as_float(); +pub fn float_ratio(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { + let f = crate::value::unboxed(recv, vm.heap()).as_float(); if f.is_nan() { return Err(cold_value("cannot convert NaN to integer ratio")); } if f.is_infinite() { return Err(VmErr::Raised(alloc::string::String::from("OverflowError: cannot convert Infinity to integer ratio"))); } let (mantissa, exp) = parts(f); let pair = if exp >= 0 || mantissa == 0 { - [crate::frontend::int::from_f64(&mut vm.heap, f)?, Val::int(1)] + [crate::frontend::int::from_f64(vm.heap_mut(), f)?, Val::int(1)] } else { // The twos the mantissa ends in cancel against the denominator. let shed = (mantissa.trailing_zeros() as i32).min(-exp); @@ -74,7 +74,7 @@ pub fn float_ratio(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let mut den = alloc::vec![0u8; k / 8 + 1]; den[0] = 1 << (k % 8); let num = if f < 0.0 { -num } else { num }; - [vm.heap.int(num as i128)?, crate::frontend::int::from_bytes(&mut vm.heap, &den, true, false)?] + [vm.heap_mut().int(num as i128)?, crate::frontend::int::from_bytes(vm.heap_mut(), &den, true, false)?] }; vm.alloc_and_push_tuple(pair.to_vec()) } @@ -87,8 +87,8 @@ fn parts(f: f64) -> (i64, i32) { } /* `x.hex()`, the float in hexadecimal, `0x1.<13 digits>p`, a subnormal led by `0x0.`. */ -pub fn float_hex(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { - let f = crate::value::unboxed(recv, &vm.heap).as_float(); +pub fn float_hex(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { + let f = crate::value::unboxed(recv, vm.heap()).as_float(); let text = if f.is_nan() { "nan".into() } else if f.is_infinite() { if f > 0.0 { "inf".into() } else { "-inf".into() } } else { let sign = if f.is_sign_negative() { "-" } else { "" }; let bits = f.to_bits(); @@ -102,7 +102,7 @@ pub fn float_hex(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { } /* `float.fromhex(s)`, the float a hexadecimal string spells, rounded half to even like any literal. */ -pub fn float_fromhex(vm: &mut VM, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn float_fromhex(vm: &mut H, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { let text = val_to_str(vm, pos[0], "fromhex() argument must be str")?; let f = parse_hex_float(text.trim()).ok_or_else(|| cold_value("invalid hexadecimal floating-point string"))?; let f = f.map_err(|()| VmErr::Raised(alloc::string::String::from("OverflowError: hexadecimal value too large to represent as a float")))?; @@ -151,9 +151,9 @@ fn parse_hex_float(s: &str) -> Option> { } /* `float.from_number(x)`, a number made a float, any other value refused. */ -pub fn float_from_number(vm: &mut VM, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { - let x = crate::value::unboxed(pos[0], &vm.heap); - let Some(f) = crate::value::num_f64(x, &vm.heap).filter(|_| !x.is_heap() || crate::frontend::int::is_big(&vm.heap, x)) else { +pub fn float_from_number(vm: &mut H, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { + let x = crate::value::unboxed(pos[0], vm.heap()); + let Some(f) = crate::value::num_f64(x, vm.heap()).filter(|_| !x.is_heap() || crate::frontend::int::is_big(vm.heap(), x)) else { return Err(VmErr::TypeMsg(crate::s!("must be real number, not ", str vm.type_name(pos[0])))); }; vm.push(Val::float(f?)); diff --git a/src/vm/methods/object.rs b/src/vm/methods/object.rs index 2b2e3e4a..57dfc4b5 100644 --- a/src/vm/methods/object.rs +++ b/src/vm/methods/object.rs @@ -28,10 +28,10 @@ pub fn exc_str(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { } /* `__getitem__` a class over dict or list reaches through `super()`, the builtin lookup past its overrides. */ -pub fn container_getitem(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn container_getitem(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { vm.keyed(pos[0])?; - let found = match vm.heap.get(recv) { - HeapObj::Dict(rc) | HeapObj::FrozenDict(rc) => rc.borrow().get(&pos[0], &vm.heap).copied(), + let found = match vm.heap().get(recv) { + HeapObj::Dict(rc) | HeapObj::FrozenDict(rc) => rc.borrow().get(&pos[0], vm.heap()).copied(), _ => return vm.get_item_builtin(recv, pos[0]), }; let Some(v) = found else { vm.require_key(pos[0])?; return Err(vm.key_error(pos[0])) }; @@ -45,7 +45,7 @@ pub fn container_setitem(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmEr Ok(()) } -pub fn container_len(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { +pub fn container_len(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let n = vm.builtin_len(recv)?; let v = vm.int_to_val(Some(n))?; vm.push(v); @@ -53,8 +53,8 @@ pub fn container_len(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> } /* Where `x` sits in range `recv`, an integral value by arithmetic, and whether `x` was an int at all. */ -fn range_pos(vm: &VM, recv: Val, x: Val) -> (Option, bool) { - let HeapObj::Range(s, e, st) = *vm.heap.get(recv) else { return (None, true) }; +fn range_pos(vm: &H, recv: Val, x: Val) -> (Option, bool) { + let HeapObj::Range(s, e, st) = *vm.heap().get(recv) else { return (None, true) }; let int = vm.as_i128(x); let k = match int { Some(k) => k, @@ -70,8 +70,8 @@ fn range_pos(vm: &VM, recv: Val, x: Val) -> (Option, bool) { /* The error `range.index` raises for a value it lacks, which quotes the value. */ fn not_in_range() -> VmErr { VmErr::Raised(alloc::string::String::from("ValueError: range.index(x): x not in range")) } -pub fn range_index(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { - if let Some(r) = crate::frontend::int::range_index(&mut vm.heap, recv, pos[0]) { +pub fn range_index(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { + if let Some(r) = crate::frontend::int::range_index(vm.heap_mut(), recv, pos[0]) { let i = r?.ok_or_else(not_in_range)?; vm.push(i); return Ok(()); @@ -83,8 +83,31 @@ pub fn range_index(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { } } -pub fn range_count(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { - let n = crate::frontend::int::range_has(&vm.heap, recv, pos[0]).unwrap_or_else(|| range_pos(vm, recv, pos[0]).0.is_some()) as i64; +pub fn range_count(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { + let n = crate::frontend::int::range_has(vm.heap(), recv, pos[0]).unwrap_or_else(|| range_pos(vm, recv, pos[0]).0.is_some()) as i64; vm.push(Val::int(n)); Ok(()) } + +// Dunders of builtin values for call_pure. +#[cfg(feature = "pure")] +pub fn op_getitem(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { + let idx = vm.plain_index(recv, pos[0]).unwrap_or(pos[0]); + container_getitem(vm, recv, &[idx]) +} + +#[cfg(feature = "pure")] +pub fn op_contains(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { + let hit = vm.contains(recv, pos[0])?; + vm.push(Val::bool(hit)); + Ok(()) +} + +#[cfg(feature = "pure")] +pub fn op_repr(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let t = vm.repr(recv); vm.alloc_and_push_str(t) } + +#[cfg(feature = "pure")] +pub fn op_str(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let t = vm.display(recv); vm.alloc_and_push_str(t) } + +#[cfg(feature = "pure")] +pub fn op_bool(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let b = vm.truthy(recv); vm.push(Val::bool(b)); Ok(()) } diff --git a/src/vm/methods/prelude.rs b/src/vm/methods/prelude.rs index 693d0b02..5fbfa46d 100644 --- a/src/vm/methods/prelude.rs +++ b/src/vm/methods/prelude.rs @@ -1,4 +1,6 @@ pub(super) use crate::vm::{VM, Val, VmErr, HeapObj, DictMap}; +pub(super) use crate::vm::natives::Builtins; +pub(super) use crate::value::Host; pub(super) use super::recv::{ recv_str, recv_str_ref, recv_bytes, recv_bytes_ref, val_to_str, list_clone, list_mut, dict_entries, dict_mut, set_clone, set_mut, set_ref, diff --git a/src/vm/methods/recv.rs b/src/vm/methods/recv.rs index 7c3bef01..b2afb068 100644 --- a/src/vm/methods/recv.rs +++ b/src/vm/methods/recv.rs @@ -1,44 +1,52 @@ use alloc::{string::String, vec::Vec}; -use crate::vm::{VM, Val, VmErr, HeapObj, DictMap}; -use crate::vm::types::{ValSet, HeapPool, cold_type}; +use crate::vm::{Val, VmErr, HeapObj, DictMap}; +use crate::vm::types::{ValSet, HeapPool, Host, cold_type}; /* Receiver-type unwrap and arity-check primitives shared by the method dispatcher in mod.rs. */ #[inline] -pub(super) fn recv_str(vm: &VM, recv: Val) -> Result { - match vm.heap.try_get(recv) { - Some(HeapObj::Str(s)) => { vm.heap.spend(s.len()); Ok(s.clone()) } +pub(super) fn recv_str(vm: &H, recv: Val) -> Result { + match vm.heap().try_get(recv) { + Some(HeapObj::Str(s)) => { vm.heap().spend(s.len()); Ok(s.clone()) } _ => Err(cold_type("method requires a string receiver")), } } /* The receiver string borrowed in place, for methods that only read it. */ #[inline] -pub(super) fn recv_str_ref<'v>(vm: &'v VM, recv: Val) -> Result<&'v str, VmErr> { - match vm.heap.try_get(recv) { - Some(HeapObj::Str(s)) => { vm.heap.spend(s.len()); Ok(s) } +pub(super) fn recv_str_ref(vm: &H, recv: Val) -> Result<&str, VmErr> { + match vm.heap().try_get(recv) { + Some(HeapObj::Str(s)) => { vm.heap().spend(s.len()); Ok(s) } _ => Err(cold_type("method requires a string receiver")), } } /* An integer argument such as a width, refused the way an index is when it is none. */ -pub(super) fn index_arg(vm: &VM, v: Val) -> Result { +pub(super) fn index_arg(vm: &H, v: Val) -> Result { if v.is_int() || v.is_bool() { return Ok(vm.as_i128(v).unwrap_or(0) as i64); } Err(VmErr::TypeMsg(crate::s!("'", str vm.type_name(v), "' object cannot be interpreted as an integer"))) } /* A width the caller grows its result to, charged before the result is made so a huge one fails cleanly. */ -pub(super) fn width_arg(vm: &VM, v: Val) -> Result { - let width = index_arg(vm, v)?.max(0) as usize; - vm.heap.reserve(width)?; - vm.heap.spend(width); +pub(super) fn width_arg(vm: &H, v: Val) -> Result { + let width = if v.is_int() { v.as_int().max(0) as usize } else { wide_width(vm, v)? }; + vm.heap().reserve(width)?; + vm.heap().spend(width); Ok(width) } +/* A width that is no inline int, a big one overflowing as CPython's ssize_t does. */ +#[cold] +#[inline(never)] +fn wide_width(vm: &H, v: Val) -> Result { + if crate::frontend::int::is_big(vm.heap(), v) { return Err(VmErr::Overflow); } + Ok(index_arg(vm, v)?.max(0) as usize) +} + /* The tab size `expandtabs` takes, 8 when not given, held in a C int. */ -pub(super) fn tab_size(vm: &VM, pos: &[Val]) -> Result { +pub(super) fn tab_size(vm: &H, pos: &[Val]) -> Result { let Some(&v) = pos.first() else { return Ok(8) }; - let n = index_arg(vm, v)?; + let n = if crate::frontend::int::is_big(vm.heap(), v) { i64::MAX } else { index_arg(vm, v)? }; if i32::try_from(n).is_err() { return Err(VmErr::Raised(String::from("OverflowError: Python int too large to convert to C int"))); } Ok(n.max(0) as usize) } @@ -63,51 +71,51 @@ pub(super) fn expand_tabs(units: impl IntoIterator(vm: &'v VM, recv: Val) -> Result<&'v [u8], VmErr> { - match vm.heap.try_get(recv) { - Some(HeapObj::Bytes(b) | HeapObj::ByteArray(b)) => { vm.heap.spend(b.len()); Ok(b) } +pub(super) fn recv_bytes_ref(vm: &H, recv: Val) -> Result<&[u8], VmErr> { + match vm.heap().try_get(recv) { + Some(HeapObj::Bytes(b) | HeapObj::ByteArray(b)) => { vm.heap().spend(b.len()); Ok(b) } _ => Err(cold_type("method requires a bytes receiver")), } } #[inline] -pub(super) fn recv_bytes(vm: &VM, recv: Val) -> Result, VmErr> { - match vm.heap.try_get(recv) { - Some(HeapObj::Bytes(b) | HeapObj::ByteArray(b)) => { vm.heap.spend(b.len()); Ok(b.clone()) } +pub(super) fn recv_bytes(vm: &H, recv: Val) -> Result, VmErr> { + match vm.heap().try_get(recv) { + Some(HeapObj::Bytes(b) | HeapObj::ByteArray(b)) => { vm.heap().spend(b.len()); Ok(b.clone()) } _ => Err(cold_type("method requires a bytes receiver")), } } #[inline] -pub(super) fn val_to_str(vm: &VM, v: Val, what: &str) -> Result { - match vm.heap.try_get(v) { - Some(HeapObj::Str(s)) => { vm.heap.spend(s.len()); Ok(s.clone()) } +pub(super) fn val_to_str(vm: &H, v: Val, what: &str) -> Result { + match vm.heap().try_get(v) { + Some(HeapObj::Str(s)) => { vm.heap().spend(s.len()); Ok(s.clone()) } _ => Err(VmErr::TypeMsg(crate::s!(str what, ", not ", str vm.type_name(v)))), } } #[inline] -pub(super) fn list_clone(vm: &VM, recv: Val) -> Result, VmErr> { - match vm.heap.try_get(recv) { - Some(HeapObj::List(rc)) => { vm.heap.spend(rc.borrow().len()); Ok(rc.borrow().clone()) } +pub(super) fn list_clone(vm: &H, recv: Val) -> Result, VmErr> { + match vm.heap().try_get(recv) { + Some(HeapObj::List(rc)) => { vm.heap().spend(rc.borrow().len()); Ok(rc.borrow().clone()) } _ => Err(cold_type("method requires a list receiver")), } } #[inline] -pub(super) fn dict_entries(vm: &VM, recv: Val) -> Result, VmErr> { - match vm.heap.try_get(recv) { - Some(HeapObj::Dict(rc) | HeapObj::FrozenDict(rc)) => { vm.heap.spend(rc.borrow().len()); Ok(rc.borrow().iter().collect()) } +pub(super) fn dict_entries(vm: &H, recv: Val) -> Result, VmErr> { + match vm.heap().try_get(recv) { + Some(HeapObj::Dict(rc) | HeapObj::FrozenDict(rc)) => { vm.heap().spend(rc.borrow().len()); Ok(rc.borrow().iter().collect()) } _ => Err(cold_type("method requires a dict receiver")), } } /* Borrow the list of `recv` for `f`, the closure cannot touch `vm`. */ #[inline] -pub(super) fn list_mut(vm: &mut VM, recv: Val, err: &'static str, f: F) -> Result +pub(super) fn list_mut(vm: &mut H, recv: Val, err: &'static str, f: F) -> Result where F: FnOnce(&mut Vec) -> Result { - match vm.heap.try_get(recv) { + match vm.heap().try_get(recv) { Some(HeapObj::List(rc)) => f(&mut rc.borrow_mut()), _ => Err(cold_type(err)), } @@ -115,54 +123,54 @@ where F: FnOnce(&mut Vec) -> Result // Same shape as `list_mut` for dict receivers, clones the Rc so `&heap` stays free for content hashing. #[inline] -pub(super) fn dict_mut(vm: &mut VM, recv: Val, err: &'static str, f: F) -> Result +pub(super) fn dict_mut(vm: &mut H, recv: Val, err: &'static str, f: F) -> Result where F: FnOnce(&mut DictMap, &HeapPool) -> Result { - let rc = match vm.heap.try_get(recv) { + let rc = match vm.heap().try_get(recv) { Some(HeapObj::Dict(rc)) => rc.clone(), _ => return Err(cold_type(err)), }; - f(&mut rc.borrow_mut(), &vm.heap) + f(&mut rc.borrow_mut(), vm.heap()) } // Snapshot a set as Vec so the heap stays free for subsequent allocations. #[inline] -pub(super) fn set_clone(vm: &VM, recv: Val) -> Result, VmErr> { - match vm.heap.get(recv) { - HeapObj::Set(rc) => { vm.heap.spend(rc.borrow().len()); Ok(rc.borrow().iter().copied().collect()) } - HeapObj::FrozenSet(s) => { vm.heap.spend(s.len()); Ok(s.iter().copied().collect()) } +pub(super) fn set_clone(vm: &H, recv: Val) -> Result, VmErr> { + match vm.heap().get(recv) { + HeapObj::Set(rc) => { vm.heap().spend(rc.borrow().len()); Ok(rc.borrow().iter().copied().collect()) } + HeapObj::FrozenSet(s) => { vm.heap().spend(s.len()); Ok(s.iter().copied().collect()) } _ => Err(cold_type("method requires a set receiver")), } } // Reads the receiver set in place, no copy. #[inline] -pub(super) fn set_ref(vm: &VM, recv: Val, f: impl FnOnce(&ValSet, &HeapPool) -> R) -> Result { - match vm.heap.try_get(recv) { - Some(HeapObj::Set(rc)) => Ok(f(&rc.borrow(), &vm.heap)), - Some(HeapObj::FrozenSet(s)) => Ok(f(s, &vm.heap)), +pub(super) fn set_ref(vm: &H, recv: Val, f: impl FnOnce(&ValSet, &HeapPool) -> R) -> Result { + match vm.heap().try_get(recv) { + Some(HeapObj::Set(rc)) => Ok(f(&rc.borrow(), vm.heap())), + Some(HeapObj::FrozenSet(s)) => Ok(f(s, vm.heap())), _ => Err(cold_type("method requires a set receiver")), } } // Same shape as `list_mut` for set receivers, clones the Rc so `&heap` stays free for content hashing. #[inline] -pub(super) fn set_mut(vm: &mut VM, recv: Val, err: &'static str, f: F) -> Result +pub(super) fn set_mut(vm: &mut H, recv: Val, err: &'static str, f: F) -> Result where F: FnOnce(&mut ValSet, &HeapPool) -> Result { - let rc = match vm.heap.try_get(recv) { + let rc = match vm.heap().try_get(recv) { Some(HeapObj::Set(rc)) => rc.clone(), _ => return Err(cold_type(err)), }; - f(&mut rc.borrow_mut(), &vm.heap) + f(&mut rc.borrow_mut(), vm.heap()) } // `Vec` from any iterable (str/range/dict/bytes/frozenset/list/tuple/set) for set ops, so every item must hash as a set element. #[inline] -pub(super) fn iter_to_vec(vm: &mut VM, v: Val) -> Result, VmErr> { set_operand(vm, v, false) } +pub(super) fn iter_to_vec(vm: &mut H, v: Val) -> Result, VmErr> { set_operand(vm, v, false) } // The items of a set operand, an unhashable one named as a set element, or only by its type for an operation that only looks them up. -pub(super) fn set_operand(vm: &mut VM, v: Val, lookup: bool) -> Result, VmErr> { +pub(super) fn set_operand(vm: &mut H, v: Val, lookup: bool) -> Result, VmErr> { let items = vm.extract_iter(v)?; for &x in &items { if lookup { vm.require_hashable(x)?; } else { vm.require_element(x)?; } diff --git a/src/vm/methods/set.rs b/src/vm/methods/set.rs index 84afe553..7395ad72 100644 --- a/src/vm/methods/set.rs +++ b/src/vm/methods/set.rs @@ -1,7 +1,7 @@ use super::prelude::*; /* A set probes as the frozenset it equals and never matches the set being changed, anything else must hash. */ -fn probes(vm: &mut VM, recv: Val, v: Val) -> Result { +fn probes(vm: &mut H, recv: Val, v: Val) -> Result { if v.0 == recv.0 { return Ok(false); } vm.require_set_probe(v)?; vm.keyed(v)?; @@ -51,12 +51,12 @@ pub fn clear(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { } // Materialize every argument iterable up front (each may run iteration code). -fn collect_args(vm: &mut VM, pos: &[Val]) -> Result>, VmErr> { +fn collect_args(vm: &mut H, pos: &[Val]) -> Result>, VmErr> { pos.iter().map(|&a| iter_to_vec(vm, a)).collect() } // The arguments of an intersection, whose items are only looked up in the set. -fn lookup_args(vm: &mut VM, pos: &[Val]) -> Result>, VmErr> { +fn lookup_args(vm: &mut H, pos: &[Val]) -> Result>, VmErr> { pos.iter().map(|&a| set_operand(vm, a, true)).collect() } @@ -69,14 +69,14 @@ pub fn update(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { vm.push(Val::none()); Ok(()) } -pub fn frozen_copy(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { vm.push(recv); Ok(()) } +pub fn frozen_copy(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { vm.push(recv); Ok(()) } -pub fn copy(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { +pub fn copy(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let items = set_clone(vm, recv)?; vm.alloc_and_push_set(items) } -pub fn union(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn union(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let args = collect_args(vm, pos)?; // alloc_and_push_set dedups by content, so concatenating every source is enough. let mut out = set_clone(vm, recv)?; @@ -84,29 +84,29 @@ pub fn union(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { vm.alloc_and_push_set(out) } -pub fn intersection(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn intersection(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let args = lookup_args(vm, pos)?; let lhs = set_clone(vm, recv)?; - let arg_sets: Vec = args.iter().map(|it| ValSet::from_vals(it, &vm.heap)).collect(); - let out: Vec = lhs.into_iter().filter(|&v| arg_sets.iter().all(|s| s.contains(v, &vm.heap))).collect(); + let arg_sets: Vec = args.iter().map(|it| ValSet::from_vals(it, vm.heap())).collect(); + let out: Vec = lhs.into_iter().filter(|&v| arg_sets.iter().all(|s| s.contains(v, vm.heap()))).collect(); vm.alloc_and_push_set(out) } -pub fn difference(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn difference(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let args = collect_args(vm, pos)?; let lhs = set_clone(vm, recv)?; - let arg_sets: Vec = args.iter().map(|it| ValSet::from_vals(it, &vm.heap)).collect(); - let out: Vec = lhs.into_iter().filter(|&v| !arg_sets.iter().any(|s| s.contains(v, &vm.heap))).collect(); + let arg_sets: Vec = args.iter().map(|it| ValSet::from_vals(it, vm.heap())).collect(); + let out: Vec = lhs.into_iter().filter(|&v| !arg_sets.iter().any(|s| s.contains(v, vm.heap()))).collect(); vm.alloc_and_push_set(out) } -pub fn symmetric_difference(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn symmetric_difference(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let lhs = set_clone(vm, recv)?; let rhs = iter_to_vec(vm, pos[0])?; - let lset = ValSet::from_vals(&lhs, &vm.heap); - let rset = ValSet::from_vals(&rhs, &vm.heap); - let mut out: Vec = lhs.iter().filter(|&&v| !rset.contains(v, &vm.heap)).copied().collect(); - out.extend(rhs.iter().filter(|&&v| !lset.contains(v, &vm.heap)).copied()); + let lset = ValSet::from_vals(&lhs, vm.heap()); + let rset = ValSet::from_vals(&rhs, vm.heap()); + let mut out: Vec = lhs.iter().filter(|&&v| !rset.contains(v, vm.heap())).copied().collect(); + out.extend(rhs.iter().filter(|&&v| !lset.contains(v, vm.heap())).copied()); vm.alloc_and_push_set(out) } @@ -140,21 +140,21 @@ pub fn symmetric_difference_update(vm: &mut VM, recv: Val, pos: &[Val]) -> Resul vm.push(Val::none()); Ok(()) } -pub fn issubset(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { - let rhs = ValSet::from_vals(&set_operand(vm, pos[0], true)?, &vm.heap); +pub fn issubset(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { + let rhs = ValSet::from_vals(&set_operand(vm, pos[0], true)?, vm.heap()); let hit = set_ref(vm, recv, |lhs, heap| lhs.iter().all(|&v| rhs.contains(v, heap)))?; vm.push(Val::bool(hit)); Ok(()) } -pub fn issuperset(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn issuperset(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let rhs = iter_to_vec(vm, pos[0])?; let hit = set_ref(vm, recv, |lhs, heap| rhs.iter().all(|&v| lhs.contains(v, heap)))?; vm.push(Val::bool(hit)); Ok(()) } -pub fn isdisjoint(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn isdisjoint(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let rhs = iter_to_vec(vm, pos[0])?; let hit = set_ref(vm, recv, |lhs, heap| !rhs.iter().any(|&v| lhs.contains(v, heap)))?; vm.push(Val::bool(hit)); diff --git a/src/vm/methods/slice.rs b/src/vm/methods/slice.rs index e9999b8b..00685e2d 100644 --- a/src/vm/methods/slice.rs +++ b/src/vm/methods/slice.rs @@ -2,12 +2,12 @@ use super::prelude::*; use crate::vm::opcodes::subscript::slice_bounds; // `slice.indices(len)`, the bounds a sequence of that length reads. -pub fn indices(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { - let &HeapObj::Slice(start, stop, step) = vm.heap.get(recv) else { return Err(cold_type("indices() requires a slice")); }; +pub fn indices(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { + let &HeapObj::Slice(start, stop, step) = vm.heap().get(recv) else { return Err(cold_type("indices() requires a slice")); }; if !pos[0].is_int() { return Err(cold_type("slice indices must be integers")); } if pos[0].as_int() < 0 { return Err(cold_value("length should not be negative")); } - let clip = |v: Val| crate::frontend::int::clip_index(&vm.heap, v); + let clip = |v: Val| crate::frontend::int::clip_index(vm.heap(), v); let (s, e, st) = slice_bounds(clip(start), clip(stop), clip(step), pos[0].as_int())?; - let t = vm.heap.alloc(HeapObj::Tuple(vec![Val::int(s), Val::int(e), Val::int(st)]))?; + let t = vm.heap_mut().alloc(HeapObj::Tuple(vec![Val::int(s), Val::int(e), Val::int(st)]))?; vm.push(t); Ok(()) } diff --git a/src/vm/methods/string.rs b/src/vm/methods/string.rs index fcfe6e14..026bd18d 100644 --- a/src/vm/methods/string.rs +++ b/src/vm/methods/string.rs @@ -3,19 +3,20 @@ use crate::s; use crate::util::uni; use core::iter; +use crate::vm::natives::Builtins; // `str.encode([encoding])`, UTF-8/ASCII only, other names error to block silent mismatches. -pub fn encode(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn encode(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let (encoding, errors) = codec_args(vm, pos)?; let out = crate::vm::codecs::encode(recv_str_ref(vm, recv)?, encoding, errors)?; - let v = vm.heap.alloc(HeapObj::Bytes(out))?; + let v = vm.heap_mut().alloc(HeapObj::Bytes(out))?; vm.push(v); Ok(()) } /* The encoding and error handler a call names, UTF-8 and strict where it names none. */ -pub(crate) fn codec_args<'v>(vm: &'v VM, pos: &[Val]) -> Result<(&'v str, &'v str), VmErr> { +pub(crate) fn codec_args<'v, H: Host>(vm: &'v H, pos: &[Val]) -> Result<(&'v str, &'v str), VmErr> { let arg = |i: usize, dflt: &'static str, what: &str| match pos.get(i) { - Some(&v) if !v.is_none() => match vm.heap.try_get(v) { + Some(&v) if !v.is_none() => match vm.heap().try_get(v) { Some(HeapObj::Str(s)) => Ok(s.as_str()), _ => Err(VmErr::TypeMsg(crate::s!(str what, "() argument must be str, not ", str vm.type_name(v)))), }, @@ -27,7 +28,7 @@ pub(crate) fn codec_args<'v>(vm: &'v VM, pos: &[Val]) -> Result<(&'v str, &'v st // str zero-arg transforms, the receiver read in place and the result pushed. macro_rules! str_transform { ($name:ident, $f:expr) => { - pub fn $name(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { + pub fn $name(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let out = $f(recv_str_ref(vm, recv)?); vm.alloc_and_push_str(out) } @@ -40,27 +41,27 @@ str_transform!(title, title_case); // str.strip / lstrip / rstrip trim whitespace, or any char in the optional arg. enum Trim { Both, Start, End } -fn strip_impl(vm: &mut VM, recv: Val, pos: &[Val], mode: Trim) -> Result<(), VmErr> { +fn strip_impl(vm: &mut H, recv: Val, pos: &[Val], mode: Trim) -> Result<(), VmErr> { let s = recv_str_ref(vm, recv)?; let out = if pos.is_empty() { match mode { Trim::Both => s.trim_matches(uni::is_space), Trim::Start => s.trim_start_matches(uni::is_space), Trim::End => s.trim_end_matches(uni::is_space) }.to_string() } else { let name = match mode { Trim::Both => "strip", Trim::Start => "lstrip", Trim::End => "rstrip" }; - let Some(HeapObj::Str(p)) = vm.heap.try_get(pos[0]) else { return Err(VmErr::TypeMsg(s!(str name, " arg must be None or str"))) }; + let Some(HeapObj::Str(p)) = vm.heap().try_get(pos[0]) else { return Err(VmErr::TypeMsg(s!(str name, " arg must be None or str"))) }; let p = p.clone(); let f = |c: char| p.contains(c); match mode { Trim::Both => s.trim_matches(f), Trim::Start => s.trim_start_matches(f), Trim::End => s.trim_end_matches(f) }.to_string() }; vm.alloc_and_push_str(out) } -pub fn strip(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { strip_impl(vm, recv, pos, Trim::Both) } -pub fn lstrip(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { strip_impl(vm, recv, pos, Trim::Start) } -pub fn rstrip(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { strip_impl(vm, recv, pos, Trim::End) } +pub fn strip(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { strip_impl(vm, recv, pos, Trim::Both) } +pub fn lstrip(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { strip_impl(vm, recv, pos, Trim::Start) } +pub fn rstrip(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { strip_impl(vm, recv, pos, Trim::End) } // str predicates, class tests need a non-empty string and case tests a cased char. macro_rules! str_pred { ($name:ident, $f:expr) => { - pub fn $name(vm: &mut VM, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { + pub fn $name(vm: &mut H, recv: Val, _pos: &[Val]) -> Result<(), VmErr> { let hit = $f(recv_str_ref(vm, recv)?); vm.push(Val::bool(hit)); Ok(()) @@ -95,8 +96,8 @@ fn title_cased(s: &str) -> bool { } // Collect the affix argument as one or more strings (str, or a tuple of str). -fn affixes(vm: &VM, v: Val, end: bool) -> Result, VmErr> { - if let Some(HeapObj::Tuple(items)) = vm.heap.try_get(v) { +fn affixes(vm: &H, v: Val, end: bool) -> Result, VmErr> { + if let Some(HeapObj::Tuple(items)) = vm.heap().try_get(v) { let items = items.clone(); let mut out = Vec::with_capacity(items.len()); let what = if end { "tuple for endswith must only contain str" } else { "tuple for startswith must only contain str" }; @@ -108,20 +109,21 @@ fn affixes(vm: &VM, v: Val, end: bool) -> Result, VmErr> { } // `str.startswith`/`endswith(affix[, start[, stop]])`, the optional window restricts where the affix must sit. -fn affix_impl(vm: &mut VM, recv: Val, pos: &[Val], end: bool) -> Result<(), VmErr> { +fn affix_impl(vm: &mut H, recv: Val, pos: &[Val], end: bool) -> Result<(), VmErr> { let affixes = affixes(vm, pos[0], end)?; - let ascii = vm.heap.str_is_ascii(recv); + let ascii = vm.heap_mut().str_is_ascii(recv); let s = recv_str_ref(vm, recv)?; - let hit = window(s, ascii, pos, 1).is_some_and(|(lo, hi)| affixes.iter().any(|p| if end { s[lo..hi].ends_with(p.as_str()) } else { s[lo..hi].starts_with(p.as_str()) })); + let hit = window(s, ascii, pos, 1, vm.heap()).is_some_and(|(lo, hi)| affixes.iter().any(|p| if end { s[lo..hi].ends_with(p.as_str()) } else { s[lo..hi].starts_with(p.as_str()) })); vm.push(Val::bool(hit)); Ok(()) } -pub fn startswith(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { affix_impl(vm, recv, pos, false) } -pub fn endswith(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { affix_impl(vm, recv, pos, true) } +pub fn startswith(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { affix_impl(vm, recv, pos, false) } +pub fn endswith(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { affix_impl(vm, recv, pos, true) } // Byte bounds of the optional char-index `start`/`stop` window (pos[from], pos[from + 1]), None when start lies past the end or past stop. -fn window(s: &str, ascii: bool, pos: &[Val], from: usize) -> Option<(usize, usize)> { - let arg = |k: usize| pos.get(k).filter(|v| v.is_int()).map(|v| v.as_int()); +fn window(s: &str, ascii: bool, pos: &[Val], from: usize, heap: &crate::value::HeapPool) -> Option<(usize, usize)> { + // A bound past i64 clips to the inline extreme, past either end of any text. + let arg = |k: usize| pos.get(k).map(|&v| crate::frontend::int::clip_index(heap, v)).filter(|v| v.is_int()).map(|v| v.as_int()); let len = s.len() as i64; // ASCII has one char per byte, other text walks chars from the end the index counts from, None past the end. let byte = |i: i64| -> Option { @@ -134,7 +136,7 @@ fn window(s: &str, ascii: bool, pos: &[Val], from: usize) -> Option<(usize, usiz (lo <= hi).then_some((lo, hi)) } -fn find_impl(vm: &mut VM, recv: Val, pos: &[Val], last: bool, raise: bool) -> Result<(), VmErr> { +fn find_impl(vm: &mut H, recv: Val, pos: &[Val], last: bool, raise: bool) -> Result<(), VmErr> { let what = match (last, raise) { (false, false) => "find() argument 1 must be str", (true, false) => "rfind() argument 1 must be str", @@ -142,9 +144,9 @@ fn find_impl(vm: &mut VM, recv: Val, pos: &[Val], last: bool, raise: bool) -> Re (true, true) => "rindex() argument 1 must be str", }; let sub = val_to_str(vm, pos[0], what)?; - let ascii = vm.heap.str_is_ascii(recv); + let ascii = vm.heap_mut().str_is_ascii(recv); let s = recv_str_ref(vm, recv)?; - let idx = window(s, ascii, pos, 1).and_then(|(lo, hi)| { + let idx = window(s, ascii, pos, 1, vm.heap()).and_then(|(lo, hi)| { let hay = &s[lo..hi]; // One character searches as a char, which finds it a byte scan at a time. let mut one = sub.chars(); @@ -160,16 +162,16 @@ fn find_impl(vm: &mut VM, recv: Val, pos: &[Val], last: bool, raise: bool) -> Re vm.push(Val::int(idx)); Ok(()) } -pub fn find(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, false, false) } -pub fn rfind(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, true, false) } -pub fn index(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, false, true) } -pub fn rindex(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, true, true) } +pub fn find(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, false, false) } +pub fn rfind(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, true, false) } +pub fn index(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, false, true) } +pub fn rindex(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { find_impl(vm, recv, pos, true, true) } -pub fn count(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn count(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let sub = val_to_str(vm, pos[0], "count() argument 1 must be str")?; - let ascii = vm.heap.str_is_ascii(recv); + let ascii = vm.heap_mut().str_is_ascii(recv); let s = recv_str_ref(vm, recv)?; - let c = window(s, ascii, pos, 1).map_or(0, |(lo, hi)| { + let c = window(s, ascii, pos, 1, vm.heap()).map_or(0, |(lo, hi)| { let hay = &s[lo..hi]; // One ASCII byte counts as a plain byte scan, which the compiler vectorizes. match sub.as_bytes() { @@ -201,7 +203,7 @@ fn split_ws_max(s: &str, m: usize, rev: bool) -> Vec { } // `str.split`/`rsplit([sep[, maxsplit]])`, `from_right` counts from the right and reverses sep-mode results. -fn split_impl(vm: &mut VM, recv: Val, pos: &[Val], from_right: bool) -> Result<(), VmErr> { +fn split_impl(vm: &mut H, recv: Val, pos: &[Val], from_right: bool) -> Result<(), VmErr> { let s = recv_str_ref(vm, recv)?; // Optional second arg is maxsplit (<0 means unlimited). let maxsplit: Option = match pos.get(1) { @@ -224,32 +226,32 @@ fn split_impl(vm: &mut VM, recv: Val, pos: &[Val], from_right: bool) -> Result<( None => s.split(sep.as_str()).map(String::from).collect(), } }; - let parts: Vec = strs.into_iter().map(|p| vm.heap.alloc(HeapObj::Str(p))).collect::>()?; + let parts: Vec = strs.into_iter().map(|p| vm.heap_mut().alloc(HeapObj::Str(p))).collect::>()?; vm.alloc_and_push_list(parts) } -pub fn split(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { split_impl(vm, recv, pos, false) } +pub fn split(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { split_impl(vm, recv, pos, false) } -pub fn join(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn join(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let items = vm.extract_iter(pos[0]).map_err(|e| if matches!(e, VmErr::TypeMsg(_)) { cold_type("can only join an iterable") } else { e })?; let sep = recv_str_ref(vm, recv)?; // The joined length is known before any copy, so a text past the memory limit fails first. let mut total = sep.len().saturating_mul(items.len().saturating_sub(1)); for (i, &v) in items.iter().enumerate() { - let Some(HeapObj::Str(s)) = vm.heap.try_get(v) else { return Err(VmErr::TypeMsg(crate::s!("sequence item ", int i as i64, ": expected str instance, ", str vm.type_name(v), " found"))) }; + let Some(HeapObj::Str(s)) = vm.heap().try_get(v) else { return Err(VmErr::TypeMsg(crate::s!("sequence item ", int i as i64, ": expected str instance, ", str vm.type_name(v), " found"))) }; total = total.saturating_add(s.len()); } - vm.heap.reserve(total)?; - vm.heap.spend(total); + vm.heap().reserve(total)?; + vm.heap().spend(total); let mut out = String::with_capacity(total); for (i, &v) in items.iter().enumerate() { if i > 0 { out.push_str(sep); } - if let Some(HeapObj::Str(s)) = vm.heap.try_get(v) { out.push_str(s); } + if let Some(HeapObj::Str(s)) = vm.heap().try_get(v) { out.push_str(s); } } vm.alloc_and_push_str(out) } -pub fn replace(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn replace(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let s = recv_str_ref(vm, recv)?; let old = val_to_str(vm, pos[0], "replace() argument 1 must be str")?; let new = val_to_str(vm, pos[1], "replace() argument 2 must be str")?; @@ -257,7 +259,7 @@ pub fn replace(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { if new.len() > old.len() { let hits = if old.is_empty() { s.chars().count() + 1 } else { s.matches(old.as_str()).count() }; let hits = match pos.get(2) { Some(n) if n.is_int() && n.as_int() >= 0 => hits.min(n.as_int() as usize), _ => hits }; - vm.heap.reserve(s.len().saturating_add(hits.saturating_mul(new.len() - old.len())))?; + vm.heap().reserve(s.len().saturating_add(hits.saturating_mul(new.len() - old.len())))?; } // Optional third arg is max replacements (<0 means all). let out = match pos.get(2) { @@ -269,7 +271,7 @@ pub fn replace(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { } // `str.rsplit([sep[, maxsplit]])`, like split but counts from the right. -pub fn rsplit(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { split_impl(vm, recv, pos, true) } +pub fn rsplit(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { split_impl(vm, recv, pos, true) } // `str.format(*args)` fills `{[idx][!r|!s|!a][:spec]}` fields, a spec may hold fields one level deep, keyword fields are not supported. /* Where named fields come from, the keywords of `format` or the mapping of `format_map`. */ @@ -444,11 +446,11 @@ fn expand_fields(vm: &mut VM, tmpl: &str, pos: &[Val], names: Names, numbering: Ok(out) } -str_transform!(casefold, |s: &str| s.to_lowercase()); +str_transform!(casefold, |s: &str| { let mut out = String::with_capacity(s.len()); s.chars().for_each(|c| crate::util::uni::push_fold(&mut out, c)); out }); str_transform!(swapcase, |s: &str| s.chars().map(|c| if c.is_uppercase() { c.to_lowercase().to_string() } else if c.is_lowercase() { c.to_uppercase().to_string() } else { c.to_string() }).collect::()); /* Parse a pad-method `(width[, fill])` arg pair, `width_err` names the method for the width TypeError. */ -fn width_and_fill(vm: &mut VM, pos: &[Val]) -> Result<(usize, char), VmErr> { +fn width_and_fill(vm: &mut H, pos: &[Val]) -> Result<(usize, char), VmErr> { let width = width_arg(vm, pos[0])?; let fill = if pos.len() > 1 { let f = val_to_str(vm, pos[1], "The fill character must be a unicode character")?; @@ -459,7 +461,7 @@ fn width_and_fill(vm: &mut VM, pos: &[Val]) -> Result<(usize, char), VmErr> { } // `str.ljust`/`rjust`/`center(width[, fill])` pad to width in code points, an odd centre pad leans left on an odd width. -fn justify(vm: &mut VM, recv: Val, pos: &[Val], align: u8) -> Result<(), VmErr> { +fn justify(vm: &mut H, recv: Val, pos: &[Val], align: u8) -> Result<(), VmErr> { let (width, fill) = width_and_fill(vm, pos)?; let s = recv_str_ref(vm, recv)?; let pad = width.saturating_sub(s.chars().count()); @@ -467,30 +469,30 @@ fn justify(vm: &mut VM, recv: Val, pos: &[Val], align: u8) -> Result<(), VmErr> let out: String = iter::repeat_n(fill, left).chain(s.chars()).chain(iter::repeat_n(fill, pad - left)).collect(); vm.alloc_and_push_str(out) } -pub fn ljust(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { justify(vm, recv, pos, b'<') } -pub fn rjust(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { justify(vm, recv, pos, b'>') } -pub fn center(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { justify(vm, recv, pos, b'^') } +pub fn ljust(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { justify(vm, recv, pos, b'<') } +pub fn rjust(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { justify(vm, recv, pos, b'>') } +pub fn center(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { justify(vm, recv, pos, b'^') } -pub fn expandtabs(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn expandtabs(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let ts = tab_size(vm, pos)?; - let out: String = expand_tabs(recv_str_ref(vm, recv)?.chars(), ['\t', '\n', '\r', ' '], ts, vm.heap.room())?.into_iter().collect(); + let out: String = expand_tabs(recv_str_ref(vm, recv)?.chars(), ['\t', '\n', '\r', ' '], ts, vm.heap().room())?.into_iter().collect(); vm.alloc_and_push_str(out) } // `str.removeprefix` / `removesuffix`, strip if present, else return unchanged. -pub fn removeprefix(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn removeprefix(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let p = val_to_str(vm, pos[0], "removeprefix() argument must be str")?; // A string without the prefix is its own result. match recv_str_ref(vm, recv)?.strip_prefix(p.as_str()).map(String::from) { Some(out) => vm.alloc_and_push_str(out), None => { vm.push(recv); Ok(()) } } } -pub fn removesuffix(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn removesuffix(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let suf = val_to_str(vm, pos[0], "removesuffix() argument must be str")?; match recv_str_ref(vm, recv)?.strip_suffix(suf.as_str()).map(String::from) { Some(out) => vm.alloc_and_push_str(out), None => { vm.push(recv); Ok(()) } } } // `str.splitlines(keepends=False)`, split on \n / \r / \r\n and the other line breaks, each kept on its line when `keepends` holds. -pub fn splitlines(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn splitlines(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { // line boundaries \n \r \r\n \v \f \x1c \x1d \x1e \x85. const BREAKS: &[char] = &['\n', '\r', '\u{0b}', '\u{0c}', '\u{1c}', '\u{1d}', '\u{1e}', '\u{85}', '\u{2028}', '\u{2029}']; let keep = pos.first().is_some_and(|&k| vm.truthy(k)); @@ -511,31 +513,31 @@ pub fn splitlines(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { } } if !cur.is_empty() { lines.push(cur); } - let parts = lines.into_iter().map(|l| vm.heap.alloc(HeapObj::Str(l))).collect::, _>>()?; + let parts = lines.into_iter().map(|l| vm.heap_mut().alloc(HeapObj::Str(l))).collect::, _>>()?; vm.alloc_and_push_list(parts) } // `str.partition` / `rpartition`, (head, sep, tail), on miss returns (s,"","") / ("","",s). -fn partition_impl(vm: &mut VM, recv: Val, pos: &[Val], from_right: bool) -> Result<(), VmErr> { +fn partition_impl(vm: &mut H, recv: Val, pos: &[Val], from_right: bool) -> Result<(), VmErr> { let sep = val_to_str(vm, pos[0], "must be str")?; if sep.is_empty() { return Err(cold_value("empty separator")); } let s = recv_str_ref(vm, recv)?; let hit = if from_right { s.rfind(sep.as_str()) } else { s.find(sep.as_str()) }; let pieces = hit.map(|i| (s[..i].to_string(), s[i + sep.len()..].to_string())); - let empty = |vm: &mut VM| vm.heap.alloc(HeapObj::Str(String::new())); + let empty = |vm: &mut H| vm.heap_mut().alloc(HeapObj::Str(String::new())); let (av, bv, cv) = match pieces { - Some((a, c)) => (vm.heap.alloc(HeapObj::Str(a))?, vm.heap.alloc(HeapObj::Str(sep))?, vm.heap.alloc(HeapObj::Str(c))?), + Some((a, c)) => (vm.heap_mut().alloc(HeapObj::Str(a))?, vm.heap_mut().alloc(HeapObj::Str(sep))?, vm.heap_mut().alloc(HeapObj::Str(c))?), // A miss keeps the original string whole on its side. None if from_right => (empty(vm)?, empty(vm)?, recv), None => (recv, empty(vm)?, empty(vm)?), }; vm.alloc_and_push_tuple(vec![av, bv, cv]) } -pub fn partition(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { partition_impl(vm, recv, pos, false) } -pub fn rpartition(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { partition_impl(vm, recv, pos, true) } +pub fn partition(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { partition_impl(vm, recv, pos, false) } +pub fn rpartition(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { partition_impl(vm, recv, pos, true) } // str padding. -pub fn zfill(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn zfill(vm: &mut H, recv: Val, pos: &[Val]) -> Result<(), VmErr> { let width = width_arg(vm, pos[0])?; let s = recv_str_ref(vm, recv)?; let nchars = s.chars().count(); @@ -546,29 +548,29 @@ pub fn zfill(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> { } // `str.maketrans`, the code point table `translate` takes, from a dict or from strings of what maps to what. -pub fn maketrans(vm: &mut VM, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { +pub fn maketrans(vm: &mut H, _recv: Val, pos: &[Val]) -> Result<(), VmErr> { let mut table = DictMap::new(); if pos.len() == 1 { - let Some(HeapObj::Dict(d)) = vm.heap.try_get(pos[0]) else { return Err(cold_type("if you give only one argument to maketrans it must be a dict")) }; + let Some(HeapObj::Dict(d)) = vm.heap().try_get(pos[0]) else { return Err(cold_type("if you give only one argument to maketrans it must be a dict")) }; let entries: Vec<(Val, Val)> = d.borrow().iter().collect(); for (k, v) in entries { - let key = match vm.heap.try_get(k) { + let key = match vm.heap().try_get(k) { Some(HeapObj::Str(t)) => match single(t) { Some(c) => Val::int(c as i64), None => return Err(cold_value("string keys in translatetable must be of length 1")) }, _ if k.is_int() => k, _ => return Err(cold_type("keys in translate table mustbe strings or integers")), }; - table.insert(key, v, &vm.heap); + table.insert(key, v, vm.heap()); } return vm.alloc_and_push_dict(table); } - let (Some(HeapObj::Str(from)), Some(HeapObj::Str(to))) = (vm.heap.try_get(pos[0]), vm.heap.try_get(pos[1])) else { + let (Some(HeapObj::Str(from)), Some(HeapObj::Str(to))) = (vm.heap().try_get(pos[0]), vm.heap().try_get(pos[1])) else { return Err(cold_type("first maketrans argument must be a string if there is a second argument")); }; if from.chars().count() != to.chars().count() { return Err(cold_value("the first two maketrans arguments must have equal length")); } let pairs: Vec<(char, char)> = from.chars().zip(to.chars()).collect(); let gone = match pos.get(2) { Some(&z) => val_to_str(vm, z, "maketrans() argument 3 must be str")?, None => String::new() }; - for (a, b) in pairs { table.insert(Val::int(a as i64), Val::int(b as i64), &vm.heap); } - for c in gone.chars() { table.insert(Val::int(c as i64), Val::none(), &vm.heap); } + for (a, b) in pairs { table.insert(Val::int(a as i64), Val::int(b as i64), vm.heap()); } + for c in gone.chars() { table.insert(Val::int(c as i64), Val::none(), vm.heap()); } vm.alloc_and_push_dict(table) } diff --git a/src/vm/mod.rs b/src/vm/mod.rs index af1ef248..c8e7c20b 100644 --- a/src/vm/mod.rs +++ b/src/vm/mod.rs @@ -1,9 +1,5 @@ -/* The value model moved to `crate::value`, kept here so existing embedder paths keep resolving. */ -#[doc(hidden)] -pub use crate::value as types; -/* The optimizer moved to `crate::optimizer`, kept here so existing embedder paths keep resolving. */ -#[doc(hidden)] -pub use crate::optimizer; +// The value model by the name the VM modules use. +pub(crate) use crate::value as types; mod cache; mod block; @@ -26,14 +22,15 @@ mod act; mod gc; mod helpers; mod init; -mod keys; -mod jobs; +pub(crate) mod keys; +pub(crate) mod jobs; +pub(crate) mod natives; mod redo; mod nest; mod object_model; mod groups; pub(crate) mod codecs; -mod memview; +pub(crate) mod memview; mod hints; mod timsort; @@ -43,7 +40,7 @@ use crate::util::hash::FxHashMap as HashMap; pub use types::{Val, HeapObj, HeapPool, VmErr, Limits}; pub use methods::{ALL_METHODS, BuiltinMethodId, MethodDesc, MethodKind, lookup_method}; pub use globals::exc_parents; -pub use scope::{FnScope, Kind}; +pub use scope::{Binding, BodyKind, FnScope, Kind}; use types::*; use cache::CachePool; @@ -566,10 +563,7 @@ impl<'a> VM<'a> { /* What a fresh VM sets up on its chunk, wide ints, budget, function tables, `__name__` and `NotImplemented`. */ fn boot(&mut self, chunk: &'a SSAChunk) { - // Python registers its wide ints before any constant boxes one. - self.heap.set_wide_int(&crate::frontend::int::KIND); - self.heap.know_kind(&crate::frontend::complex::KIND); - self.heap.know_kind(&crate::frontend::sentinel::KIND); + crate::frontend::install(&mut self.heap); self.heap.set_budget(self.budget); self.build_function_table(chunk, None, None); self.index_functions(0); diff --git a/src/vm/natives.rs b/src/vm/natives.rs new file mode 100644 index 00000000..3a86210d --- /dev/null +++ b/src/vm/natives.rs @@ -0,0 +1,1370 @@ +use alloc::{string::{String, ToString}, vec::Vec}; +use super::types::*; +use super::jobs::{Check, Folded, Gather, Takes, Total, take_plain}; +use super::keys::{PairScan, scan_pairs}; +use super::VM; +use crate::parser::{OpCode, SSAChunk}; +use alloc::rc::Rc; +use crate::s; +use core::cell::RefCell; +use super::globals::numeric::{float_arg, float_to_int, int_arg, parse_int_radix, round_float_tens}; + +// Arguments and user code hooks a builtin function reads past the host. +pub(crate) trait Frames: Host + Sized { + type Code: ?Sized; + fn pop(&mut self) -> Result; + fn pop_n(&mut self, n: usize) -> Result, VmErr>; + // Whether this call may open a frame of the loop. + fn take_job_ok(&mut self) -> bool { false } + fn check_job(&mut self, _o: Val, _check: Check) -> bool { false } + fn render_job(&mut self, _o: Val, _print: bool, _chunk: &Self::Code) -> Result { Ok(false) } + fn str_job(&mut self, _o: Val, _print: bool) -> bool { false } + fn try_call_dunder(&mut self, _recv: Val, _name: &str, _args: &[Val], _chunk: &Self::Code) -> Result, VmErr> { Ok(None) } + fn lookup_class_member(&self, _cls: Val, _name: &str) -> Option<(Val, Val)> { None } + // The result a call left. + fn last(&self) -> Option; + fn stack(&mut self) -> &mut Vec; + fn is_async_object(&self, _v: Val) -> bool { false } + // Pops positional and flat keyword arguments. + fn parse_call_args(&mut self, operand: u16) -> Result<(Vec, Vec), VmErr> { + let mut all = self.pop_n((operand & 0xFF) as usize + 2 * ((operand >> 8) & 0xFF) as usize)?; + let kw = all.split_off((operand & 0xFF) as usize); + Ok((all, kw)) + } + // Whether stepping `o` may run user code. + fn may_job(&self, o: Val) -> bool { o.is_heap() && matches!(self.heap().get(o), HeapObj::Coroutine(_) | HeapObj::Instance(..) | HeapObj::Iter(..)) } + fn items_job(&mut self, _o: Val, _takes: Takes, _chunk: &Self::Code) -> Result { Ok(false) } + fn gather_gen(&mut self, _o: Val, _takes: Takes, _extra: &[Val]) -> Result { Ok(false) } + // The key `k` bound to a function of the loop when it steps a generator among `items` in Rust, None when it steps none. + fn held_key(&mut self, _k: Val, _items: &[Val]) -> Result, VmErr> { Ok(None) } + // Gathers the iterable at stack index `base` of builtin `id` by a job of the loop, whether the job took the call. + fn gather_call(&mut self, _id: NativeFnId, _op: u16, _base: usize, _chunk: &Self::Code) -> Result { Ok(false) } + fn iter_to_vec_op(&mut self, _obj: Val, _chunk: &Self::Code) -> Result>, VmErr> { Ok(None) } + // A fold of plain items counts this down per item. + fn budget_mut(&mut self) -> &mut u64; + // Split borrow for a loop reading the heap and counting the budget. + fn heap_budget(&mut self) -> (&HeapPool, &mut u64); + fn charge_step(&mut self) -> Result<(), VmErr> { let n = 1 + self.heap().take_work(); self.charge(n) } + // Only the VM continues a fold as a job. + fn fold_run(&mut self, o: Val, takes: Takes, extra: &[Val], _frames: bool, chunk: &Self::Code) -> Result<(), VmErr> { + let mut rest = None; + self.fold_ready(o, takes, extra, chunk, &mut rest)?; + match rest { None => Ok(()), Some(_) => Err(cold_runtime("a fold that runs user code")) } + } + fn try_binary_dunder(&mut self, _op: OpCode, _a: Val, _b: Val, _inplace: bool, _chunk: &Self::Code) -> Result, VmErr> { Ok(None) } + fn try_compare_dunder(&mut self, _op: OpCode, _a: Val, _b: Val, _chunk: &Self::Code) -> Result, VmErr> { Ok(None) } + // Sequences holding instances walk through their dunders. + fn values_lt(&mut self, a: Val, b: Val, _chunk: &Self::Code) -> Result { self.lt_vals(a, b) } + fn exec_call(&mut self, _operand: u16, _chunk: &Self::Code) -> Result<(), VmErr> { Err(cold_runtime("a call of user code")) } + fn call_rows(&mut self, _f: Val, _cols: &[Vec], _n: usize, _chunk: &Self::Code) -> Result, VmErr> { Err(cold_runtime("a call of user code")) } + // Builtin iterators step as the VM steps them. + fn next_of(&mut self, it: Val, chunk: &Self::Code) -> Result, VmErr> { + let Some(HeapObj::Iter(frame, _)) = self.heap().try_get(it) else { return Err(VmErr::TypeMsg(s!("'", str self.type_name(it), "' object is not an iterator"))) }; + let frame = frame.clone(); + let state = frame.borrow().clone(); + match state { + IterFrame::Zip { its, strict } => self.zip_step(&its, strict, chunk), + IterFrame::Enumerate { it, n } => self.enumerate_step(Some(&frame), it, n, chunk), + _ => frame.borrow_mut().next_item(self.heap_mut()), + } + } + fn merge_job(&mut self, items: Vec, _reverse: bool, _into: Val) -> Result<(), Vec> { Err(items) } + fn complex_job(&mut self, _v: Val) -> Result<(), VmErr> { Err(cold_runtime("a call of user code")) } + fn keys_floor(&mut self, _key: Val, _items: Vec, _is_min: bool) -> Result<(), VmErr> { Err(cold_runtime("a call of user code")) } + // Containers render with the __repr__ of instances they hold. + fn render_now(&mut self, v: Val, _chunk: &Self::Code, seen: &mut Vec) -> Result { Ok(self.repr_d(v, seen)) } +} + +impl<'a> Frames for VM<'a> { + type Code = SSAChunk; + #[inline(always)] fn pop(&mut self) -> Result { VM::pop(self) } + #[inline(always)] fn pop_n(&mut self, n: usize) -> Result, VmErr> { VM::pop_n(self, n) } + #[inline(always)] fn take_job_ok(&mut self) -> bool { core::mem::take(&mut self.job_ok) } + #[inline] fn check_job(&mut self, o: Val, check: Check) -> bool { VM::check_job(self, o, check) } + #[inline] fn render_job(&mut self, o: Val, print: bool, chunk: &SSAChunk) -> Result { VM::render_job(self, o, print, chunk) } + #[inline] fn str_job(&mut self, o: Val, print: bool) -> bool { VM::str_job(self, o, print) } + #[inline] fn try_call_dunder(&mut self, recv: Val, name: &str, args: &[Val], chunk: &SSAChunk) -> Result, VmErr> { VM::try_call_dunder(self, recv, name, args, chunk) } + #[inline] fn lookup_class_member(&self, cls: Val, name: &str) -> Option<(Val, Val)> { VM::lookup_class_member(self, cls, name) } + #[inline] fn last(&self) -> Option { self.stack.last().copied() } + #[inline(always)] fn stack(&mut self) -> &mut Vec { &mut self.stack } + #[inline] fn is_async_object(&self, v: Val) -> bool { VM::is_async_object(self, v) } + #[inline] fn parse_call_args(&mut self, operand: u16) -> Result<(Vec, Vec), VmErr> { VM::parse_call_args(self, operand) } + #[inline] fn may_job(&self, o: Val) -> bool { VM::may_job(self, o) } + #[inline] fn items_job(&mut self, o: Val, takes: Takes, chunk: &SSAChunk) -> Result { VM::items_job(self, o, takes, chunk) } + #[inline] fn gather_gen(&mut self, o: Val, takes: Takes, extra: &[Val]) -> Result { VM::gather_gen(self, o, takes, extra) } + fn held_key(&mut self, k: Val, items: &[Val]) -> Result, VmErr> { + if !self.steps_whole(k) { return Ok(None); } + let leaf = |vm: &Self, vs: &[Val]| vs.iter().any(|&v| vm.steps_py(v)); + let held = match items[..] { + [o] => match self.heap.try_get(o) { Some(HeapObj::List(rc)) => { let l = rc.borrow().clone(); leaf(self, &l) }, Some(HeapObj::Tuple(t)) => leaf(self, t), _ => true }, + _ => leaf(self, items), + }; + if !held { return Ok(None); } + Ok(Some(self.through_loop(k, 1)?.unwrap_or(k))) + } + fn gather_call(&mut self, id: NativeFnId, op: u16, base: usize, chunk: &SSAChunk) -> Result { + self.job_ok = true; + if self.gather_arg(id, op, base, chunk)? { return Ok(true); } + self.job_ok = false; + Ok(false) + } + #[inline] fn iter_to_vec_op(&mut self, obj: Val, chunk: &SSAChunk) -> Result>, VmErr> { VM::iter_to_vec_op(self, obj, chunk) } + #[inline(always)] fn budget_mut(&mut self) -> &mut u64 { &mut self.budget } + #[inline(always)] fn heap_budget(&mut self) -> (&HeapPool, &mut u64) { (&self.heap, &mut self.budget) } + #[inline(always)] fn charge_step(&mut self) -> Result<(), VmErr> { VM::charge_step(self) } + #[inline] fn fold_run(&mut self, o: Val, takes: Takes, extra: &[Val], frames: bool, chunk: &SSAChunk) -> Result<(), VmErr> { VM::fold_run(self, o, takes, extra, frames, chunk) } + #[inline] fn try_binary_dunder(&mut self, op: OpCode, a: Val, b: Val, inplace: bool, chunk: &SSAChunk) -> Result, VmErr> { VM::try_binary_dunder(self, op, a, b, inplace, chunk) } + #[inline] fn try_compare_dunder(&mut self, op: OpCode, a: Val, b: Val, chunk: &SSAChunk) -> Result, VmErr> { VM::try_compare_dunder(self, op, a, b, chunk) } + #[inline] fn values_lt(&mut self, a: Val, b: Val, chunk: &SSAChunk) -> Result { VM::values_lt(self, a, b, chunk) } + #[inline] fn exec_call(&mut self, operand: u16, chunk: &SSAChunk) -> Result<(), VmErr> { VM::exec_call(self, operand, chunk) } + #[inline] fn call_rows(&mut self, f: Val, cols: &[Vec], n: usize, chunk: &SSAChunk) -> Result, VmErr> { VM::call_rows(self, f, cols, n, chunk) } + #[inline] fn next_of(&mut self, it: Val, chunk: &SSAChunk) -> Result, VmErr> { VM::next_of(self, it, chunk) } + #[inline] fn merge_job(&mut self, items: Vec, reverse: bool, into: Val) -> Result<(), Vec> { VM::merge_job(self, items, reverse, into) } + fn complex_job(&mut self, v: Val) -> Result<(), VmErr> { + if let Some(r) = self.calls_run(alloc::boxed::Box::new(super::globals::numeric::ComplexOf { v, hook: 0 }), true)? { self.push(r); } + Ok(()) + } + #[inline] fn keys_floor(&mut self, key: Val, items: Vec, is_min: bool) -> Result<(), VmErr> { VM::keys_floor(self, key, items, is_min) } + #[inline] fn render_now(&mut self, v: Val, chunk: &SSAChunk, seen: &mut Vec) -> Result { VM::render_now(self, v, chunk, seen) } +} + +// Builtin functions with one body for the VM and call_pure. +pub(crate) trait Builtins: Frames { + fn call_abs(&mut self, chunk: &Self::Code) -> Result<(), VmErr> { + let ok = self.take_job_ok(); + let o = self.pop()?; + // An instance's `__abs__` runs as a frame of the loop. + if ok && self.is_instance(o) && self.check_job(o, crate::vm::jobs::Check::Abs) { return Ok(()); } + // Honor a user `__abs__` and pass any return type through. + if let Some(r) = self.try_call_dunder(o, "__abs__", &[], chunk)? { + self.push(r); + return Ok(()); + } + let v = if o.is_float() { + Val::float(o.as_float().abs()) + } else if o.is_int() || o.is_bool() { + Val::int(self.as_i128(o).unwrap_or(0).abs() as i64) + } else if let Some(r) = self.heap_mut().foreign_unop(UnOp::Abs, o)? { + r + } else { + return Err(self.bad_operand("abs()", o)); + }; + self.push(v); Ok(()) + } + + #[inline(never)] + fn call_int(&mut self, argc: u16, chunk: &Self::Code) -> Result<(), VmErr> { + let ok = self.take_job_ok(); + // Two-arg form `int(string, base)` parses the string in the given radix. + if argc == 2 { + let base_v = self.pop()?; + let o = self.pop()?; + if !base_v.is_int() { return Err(cold_type("int() base must be an integer")); } + let base = base_v.as_int(); + let s = match self.heap().try_get(o) { + Some(HeapObj::Str(s)) => s.clone(), + Some(HeapObj::Bytes(b) | HeapObj::ByteArray(b)) => core::str::from_utf8(b).map_err(|_| self.invalid_int(cold_value("invalid literal"), o, base))?.into(), + _ => return Err(cold_type("int() can't convert non-string with explicit base")), + }; + let v = parse_int_radix(self.heap_mut(), &s, base).map_err(|e| self.invalid_int(e, o, base))?; + self.push(v); return Ok(()); + } + if argc == 0 { self.push(Val::int(0)); return Ok(()); } + if argc > 2 { for _ in 0..argc { let _ = self.pop(); } return Err(cold_type("int() takes at most 2 arguments")); } + let o = self.pop()?; + let o = self.plain_number(o, "__int__", chunk)?; + // An int passes through unchanged. + if o.is_int() || crate::frontend::int::is_big(self.heap(), o) { self.push(o); return Ok(()); } + let v = if o.is_float() { float_to_int(self.heap_mut(), o.as_float(), ftrunc)? } + else if o.is_bool() { Val::int(o.as_bool() as i64) } + else if o.is_heap() && let HeapObj::Str(s) = self.heap().get(o) { + let s = s.clone(); + parse_int_radix(self.heap_mut(), &s, 10).map_err(|e| self.invalid_int(e, o, 10))? + } + else if let Some(b) = self.heap().bytes_of(o) { + let text = core::str::from_utf8(b).map(String::from).map_err(|_| cold_value("invalid literal")); + text.and_then(|t| parse_int_radix(self.heap_mut(), &t, 10)).map_err(|e| self.invalid_int(e, o, 10))? + } + else if o.is_heap() && matches!(self.heap().get(o), HeapObj::Instance(..)) { + // A user `__int__` runs as a frame of the loop, its int checked as it returns. + if ok && self.check_job(o, crate::vm::jobs::Check::Int(o)) { return Ok(()); } + // Honor a user `__int__` method. + match self.try_call_dunder(o, "__int__", &[], chunk)? { + Some(r) if r.is_bool() => Val::int(r.as_bool() as i64), + Some(r) if r.is_int() || crate::frontend::int::is_big(self.heap(), r) => r, + Some(_) => return Err(cold_type("__int__ returned non-int")), + None => return Err(int_arg(self, o)), + } + } + else { return Err(int_arg(self, o)); }; + self.push(v); Ok(()) + } + + /* The number in `o`, a value of a class over int or float, through the class `hook` when it defines one. */ + fn plain_number(&mut self, o: Val, hook: &str, chunk: &Self::Code) -> Result { + let Some(&HeapObj::Boxed(n)) = self.heap().try_get(o) else { return Ok(o) }; + Ok(self.try_call_dunder(o, hook, &[], chunk)?.map_or(n, |r| crate::value::unboxed(r, self.heap()))) + } + + /* An invalid literal error that quotes `o` and names `base` like Python, any other error as it is. */ + fn invalid_int(&self, e: VmErr, o: Val, base: i64) -> VmErr { + match e { + VmErr::Value(m) if m.contains("invalid literal") => VmErr::Raised(crate::s!("ValueError: invalid literal for int() with base ", int base, ": ", str &self.repr(o))), + e => e, + } + } + + /* Converts int or parseable string to floating point. */ + #[inline(never)] + fn call_float(&mut self, argc: u16, chunk: &Self::Code) -> Result<(), VmErr> { + let ok = self.take_job_ok(); + if argc == 0 { self.push(Val::float(0.0)); return Ok(()); } // `float()` is 0.0. + let o = self.pop()?; + let o = self.plain_number(o, "__float__", chunk)?; + // A float comes back as the same object. + if o.is_float() { self.push(o); return Ok(()); } + let text = match self.heap().try_get(o) { + Some(HeapObj::Str(s)) => Some(s.clone()), + _ => self.heap().bytes_of(o).map(|b| String::from_utf8_lossy(b).into_owned()), + }; + let f = if let Some(f) = num_f64(o, self.heap()) { f? } + else if let Some(s) = text { + let t = crate::util::uni::number_text(&s); + let t = t.as_ref(); + // Accept `_` digit separators, strip then parse. + let cleaned = if t.contains('_') { + if t.starts_with('_') || t.ends_with('_') || t.contains("__") { return Err(self.no_float(o)); } + t.chars().filter(|&c| c != '_').collect::() + } else { alloc::string::String::from(t) }; + cleaned.parse().map_err(|_| self.no_float(o))? + } + else if o.is_heap() && matches!(self.heap().get(o), HeapObj::Instance(..)) { + // A user `__float__` runs as a frame of the loop, its float checked as it returns. + if ok && self.check_job(o, crate::vm::jobs::Check::Float(o)) { return Ok(()); } + // Honor a user `__float__` and fall back to `__index__` like CPython. + match self.try_call_dunder(o, "__float__", &[], chunk)? { + Some(r) if r.is_float() => { self.push(r); return Ok(()); } + Some(r) => return Err(self.returned_wrong(o, "__float__", "a float", r)), + None => match self.try_call_dunder(o, "__index__", &[], chunk)? { + Some(r) if r.is_int() || r.is_bool() || crate::frontend::int::is_big(self.heap(), r) => num_f64(r, self.heap()).unwrap_or(Ok(0.0))?, + Some(r) => return Err(self.returned_wrong(o, "__index__", "an int", r)), + None => return Err(float_arg(self, o)), + }, + } + } + else { return Err(float_arg(self, o)); }; + self.push(Val::float(f)); Ok(()) + } + + /* `complex(real, imag)` from numbers or one string, a value whose class defines `__complex__` asked first. */ + fn call_complex(&mut self, argc: u16, chunk: &Self::Code) -> Result<(), VmErr> { + use crate::frontend::complex::{Complex, parse, parts, val}; + let ok = self.take_job_ok(); + let args = self.pop_n(argc as usize)?; + // Called where a frame may open, the dunder that converts an instance runs as a frame of the loop. + if ok && argc == 1 && let Some(&a) = args.first() && self.is_instance(a) { return self.complex_job(a); } + let num = |vm: &mut Self, v: Val, which: &str| -> Result { + if let Some(z) = parts(vm.heap(), crate::value::unboxed(v, vm.heap())) { return Ok(z); } + for hook in ["__complex__", "__float__", "__index__"] { + if let Some(r) = vm.try_call_dunder(v, hook, &[], chunk)? && let Some(z) = parts(vm.heap(), r) { return Ok(z); } + } + Err(VmErr::TypeMsg(match which { + "" => crate::s!("complex() argument must be a string or a number, not ", str vm.type_name(v)), + part => crate::s!("complex() argument '", str part, "' must be a real number, not ", str vm.type_name(v)), + })) + }; + let text = |vm: &Self, v: Val| match vm.heap().try_get(v) { Some(HeapObj::Str(s)) => Some(s.clone()), _ => None }; + let z = match args.as_slice() { + [] => Complex { re: 0.0, im: 0.0 }, + [s] if text(self, *s).is_some() => parse(&text(self, *s).unwrap_or_default()).ok_or_else(|| cold_value("complex() arg is a malformed string"))?, + [a] => num(self, *a, "")?, + [a, b] => { + let (x, y) = (num(self, *a, "real")?, num(self, *b, "imag")?); + Complex { re: x.re - y.im, im: x.im + y.re } + } + _ => return Err(cold_type("complex() takes at most 2 arguments")), + }; + let v = val(self.heap_mut(), z.re, z.im)?; + self.push(v); + Ok(()) + } + + fn no_float(&self, o: Val) -> VmErr { + VmErr::Raised(crate::s!("ValueError: could not convert string to float: ", str &self.repr(o))) + } + + fn call_chr(&mut self) -> Result<(), VmErr> { + let o = self.pop()?; + if !(o.is_int() || o.is_bool() || crate::frontend::int::is_big(self.heap(), o)) { return Err(VmErr::TypeMsg(crate::s!("'", str self.type_name(o), "' object cannot be interpreted as an integer"))); } + let c = self.as_i128(o).and_then(|i| u32::try_from(i).ok()).and_then(char::from_u32).ok_or_else(|| cold_value("chr() arg not in range(0x110000)"))?; + let mut s = String::with_capacity(4); + s.push(c); + self.alloc_and_push_str(s) + } + + fn call_ord(&mut self) -> Result<(), VmErr> { + let o = self.pop()?; + if o.is_heap() + && let HeapObj::Str(s) = self.heap().get(o) { + let mut cs = s.chars(); + if let (Some(c), None) = (cs.next(), cs.next()) { + self.push(Val::int(c as i64)); return Ok(()); + } + } + // ord() of a one-byte bytes returns the byte value. + if let Some(b) = self.heap().bytes_of(o) && b.len() == 1 { + let v = b[0] as i64; + self.push(Val::int(v)); return Ok(()); + } + let found = match self.heap().try_get(o) { + Some(HeapObj::Str(s)) => s.chars().count(), + _ => match self.heap().bytes_of(o) { Some(b) => b.len(), None => return Err(VmErr::TypeMsg(crate::s!("ord() expected string of length 1, but ", str self.type_name(o), " found"))) }, + }; + Err(VmErr::TypeMsg(crate::s!("ord() expected a character, but string of length ", int found as i64, " found"))) + } + + #[inline(never)] + fn call_round(&mut self, op: u16) -> Result<(), VmErr> { + let args = self.pop_n(op as usize)?; + let v = match (args.first().copied(), args.get(1).copied()) { + (Some(o), Some(n)) if o.is_float() && n.is_int() => { + let x = o.as_float(); + let nd = n.as_int(); + if !x.is_finite() { + Val::float(x) + } else if nd >= 0 { + // Correctly-rounded decimal (round-half-even on the true value), avoids the double-rounding `x*10^n` would introduce (e.g. round(2.675, 2) -> 2.67). + let s = alloc::format!("{:.*}", (nd as usize).min(323), x); + Val::float(s.parse().unwrap_or(x)) + } else { + let r = round_float_tens(x, nd.unsigned_abs().min(309) as usize); + if r.is_infinite() { return Err(VmErr::Raised(String::from("OverflowError: rounded value too large to represent"))); } + Val::float(r) + } + } + (Some(o), None) if o.is_float() => { + // 1-arg round returns an int, promote via int_to_val so large results don't wrap. + float_to_int(self.heap_mut(), o.as_float(), fround)? + } + // Ints/bools round to themselves, negative ndigits round to tens/hundreds. + (Some(o), n) if o.is_bool() || o.is_int() || crate::frontend::int::is_big(self.heap(), o) => { + let o = if o.is_bool() { Val::int(o.as_bool() as i64) } else { o }; + match n { + // An inline int and at most 18 dropped digits stay in i64, half to even. + Some(n) if o.is_int() && (-18..0).contains(&n.as_int()) => { + let (x, f) = (o.as_int(), 10i64.pow(n.as_int().unsigned_abs() as u32)); + let (q, r) = (x.div_euclid(f), x.rem_euclid(f)); + self.heap_mut().int(((q + (2 * r > f || (2 * r == f && q % 2 != 0)) as i64) * f) as i128)? + } + Some(n) if n.is_int() && n.as_int() < 0 => crate::frontend::int::round_tens(self.heap_mut(), o, n.as_int().unsigned_abs()).unwrap_or(Ok(o))?, + _ => o, + } + } + _ => return Err(cold_type("round() requires a number")), + }; + self.push(v); Ok(()) + } + + #[inline(never)] + fn call_range(&mut self, op: u16) -> Result<(), VmErr> { + let args = self.pop_n(op as usize)?; + // A bound past i64 makes a wide range. + let (s, e, st) = match args[..] { [e] => (Val::int(0), e, Val::int(1)), [s, e] => (s, e, Val::int(1)), [s, e, st] => (s, e, st), _ => return Err(cold_type("range() takes 1 to 3 arguments")) }; + if let Some(r) = crate::frontend::int::new_range(self.heap_mut(), s, e, st) { let r = r?; self.push(r); return Ok(()); } + // Accept any integer, bools included, that fits the i64 range bounds. + // The first bound that is no integer names its type. + if let Some(&bad) = args.iter().find(|&&a| self.as_i128(a).is_none() || a.is_float()) { + return Err(VmErr::TypeMsg(crate::s!("'", str self.type_name(bad), "' object cannot be interpreted as an integer"))); + } + let gi = |i: Option| -> Result { + match i { + Some(n) if (i64::MIN as i128..=i64::MAX as i128).contains(&n) => Ok(n as i64), + Some(_) => Err(cold_overflow()), + None => Err(cold_type("range() arguments must be integers")), + } + }; + let (s, e, st) = match args.len() { + 1 => (0, gi(self.as_i128(args[0]))?, 1), + 2 => (gi(self.as_i128(args[0]))?, gi(self.as_i128(args[1]))?, 1), + 3 => (gi(self.as_i128(args[0]))?, gi(self.as_i128(args[1]))?, gi(self.as_i128(args[2]))?), + _ => return Err(cold_type("range() takes 1 to 3 arguments")), + }; + if st == 0 { return Err(cold_value("range() arg 3 must not be zero")); } + let val = self.heap_mut().alloc(HeapObj::Range(s, e, st))?; + self.push(val); Ok(()) + } + + fn call_bin(&mut self) -> Result<(), VmErr> { self.call_radix(2, "0b") } + + fn call_oct(&mut self) -> Result<(), VmErr> { self.call_radix(8, "0o") } + + fn call_hex(&mut self) -> Result<(), VmErr> { self.call_radix(16, "0x") } + + /* Push int as "" with optional sign. */ + fn call_radix(&mut self, radix: u32, prefix: &str) -> Result<(), VmErr> { + let o = self.pop()?; + let Some(d) = crate::frontend::int::radix_digits(self.heap(), o, radix) else { return Err(cold_type("integer required")); }; + let (neg, digits) = d?; + self.alloc_and_push_str(crate::s!(str if neg { "-" } else { "" }, str prefix, str &digits)) + } + + /* `str(v)` semantics, instance `__str__` wins, then `__repr__`, else the built-in display. */ + fn display_op(&mut self, v: Val, chunk: &Self::Code) -> Result { + if self.instance_class(v).is_some() { + if let Some(r) = self.try_call_dunder(v, "__str__", &[], chunk)? { + return self.require_str(r, "__str__"); + } + if let Some(r) = self.try_call_dunder(v, "__repr__", &[], chunk)? { + return self.require_str(r, "__repr__"); + } + } + // Containers render their elements with repr, dispatching user __repr__ on instances. + if self.is_container_val(v) { return self.repr_op(v, chunk); } + let s = match self.heap().foreign_repr(v) { Some(r) => r?, None => self.display(v) }; + // Render is O(size). Charge it so reprinting growing data can't outrun the budget. + self.charge(s.len())?; + Ok(s) + } + + /* `repr(v)` semantics, instance `__repr__` wins, otherwise the built-in repr (which adds quotes for strings, etc.). */ + fn repr_op(&mut self, v: Val, chunk: &Self::Code) -> Result { + let s = self.repr_deep(v, chunk, &mut Vec::new())?; + self.charge(s.len())?; + Ok(s) + } + + /* Container-aware repr dispatches `__repr__` on nested instances. Elements always use repr, with `seen` tracking heap ids for cycle detection. */ + fn repr_deep(&mut self, v: Val, chunk: &Self::Code, seen: &mut Vec) -> Result { + if let Some(r) = self.heap().foreign_repr(v) { return r; } + if !v.is_heap() { return Ok(self.repr(v)); } + if self.heap().classed(v).is_some() && let Some(r) = self.try_call_dunder(v, "__repr__", &[], chunk)? { + return self.require_str(r, "__repr__"); + } + if !self.is_container_val(v) { + if matches!(self.heap().get(v), HeapObj::Instance(..)) + && let Some(r) = self.try_call_dunder(v, "__repr__", &[], chunk)? { + return self.require_str(r, "__repr__"); + } + // The outer path goes along, so an exception inside its own args stops there. + return Ok(self.repr_d(v, seen)); + } + let id = v.as_heap(); + if seen.contains(&id) { + return Ok(match self.heap().get(v) { + HeapObj::Dict(_) | HeapObj::Set(_) => "{...}".into(), + HeapObj::FrozenDict(_) => "frozendict({...})".into(), + HeapObj::Tuple(_) => "(...)".into(), + HeapObj::FrozenSet(_) => "frozenset({...})".into(), + _ => "[...]".into(), + }); + } + if seen.len() > crate::vm::value_ops::RENDER_DEPTH_MAX { return Ok("...".into()); } + self.render_now(v, chunk, seen) + } + + fn is_container_val(&self, v: Val) -> bool { + v.is_heap() && matches!( + self.heap().get(v), + HeapObj::List(_) | HeapObj::Tuple(_) | HeapObj::Dict(_) | HeapObj::FrozenDict(_) | HeapObj::Set(_) | HeapObj::FrozenSet(_) + ) + } + + fn require_str(&self, v: Val, name: &str) -> Result { + if v.is_heap() && let HeapObj::Str(s) = self.heap().get(v) { return Ok(s.clone()); } + Err(VmErr::TypeMsg(crate::s!("'", str name, "' did not return a string"))) + } + + /* `format(v, spec)`, an instance `__format__(spec)` wins, an empty spec is `str(v)` and the rest runs the spec engine. */ + fn format_op(&mut self, v: Val, spec: &str, chunk: &Self::Code) -> Result { + if self.instance_class(v).is_some() { + let spec_val = self.heap_mut().alloc(HeapObj::Str(spec.to_string()))?; + if let Some(r) = self.try_call_dunder(v, "__format__", &[spec_val], chunk)? { + if !matches!(self.heap().try_get(r), Some(HeapObj::Str(_))) { return Err(VmErr::TypeMsg(crate::s!("__format__ must return a str, not ", str self.type_name(r)))); } + return self.require_str(r, "__format__"); + } + } + let v = crate::value::unboxed(v, self.heap()); + if spec.is_empty() { return self.display_op(v, chunk); } + if let Some(z) = crate::frontend::complex::parts(self.heap(), v).filter(|_| crate::frontend::complex::is_complex(self.heap(), v)) { + return crate::vm::format_spec::format_complex(z.re, z.im, spec, self.heap()); + } + // Only numbers and strings read a spec, `f"{x!s:>9}"` pads anything else. + let own = v.is_int() || v.is_float() || v.is_bool() || crate::frontend::int::is_big(self.heap(), v) || matches!(self.heap().try_get(v), Some(HeapObj::Str(_))); + if !own { return Err(VmErr::TypeMsg(crate::s!("unsupported format string passed to ", str &self.type_repr_name(v), ".__format__"))); } + crate::vm::format_spec::format_typed(v, spec, self.type_name(v), self.heap()) + } + + /* Python `bool()` semantics, try `__bool__`, then `__len__` (0 = False), else default True for instances. Pass-through for built-in types. */ + fn truthy_op(&mut self, v: Val, chunk: &Self::Code) -> Result { + if self.instance_class(v).is_none() { + let t = self.truthy(v); + if t && v.is_heap() && self.heap().is_not_implemented(v) { return Err(cold_type("NotImplemented should not be used in a boolean context")); } + return Ok(t); + } + if let Some(r) = self.try_call_dunder(v, "__bool__", &[], chunk)? { + if !matches!(r, x if x.is_bool()) { + return Err(VmErr::TypeMsg(crate::s!("__bool__ should return bool, returned ", str self.type_name(r)))); + } + return Ok(r.as_bool()); + } + if let Some(r) = self.try_call_dunder(v, "__len__", &[], chunk)? { + return self.len_to_bool(r); + } + Ok(self.truthy(v)) + } + + fn call_len(&mut self, chunk: &Self::Code) -> Result<(), VmErr> { + let ok = self.take_job_ok(); + let o = self.pop()?; + // An instance's `__len__` runs as a frame of the loop. + if ok && self.is_instance(o) && self.check_job(o, crate::vm::jobs::Check::Len) { return Ok(()); } + // instance `__len__` takes precedence over built-in length rules. + if let Some(r) = self.try_call_dunder(o, "__len__", &[], chunk)? { + let n = if r.is_int() { r.as_int() as i128 } + else if let Some(i) = crate::vm::types::as_i128(r, self.heap()) { i } + else { return Err(cold_type("__len__ must return int")); }; + if n < 0 { return Err(cold_value("__len__() should return >= 0")); } + let v = self.int_to_val(Some(n))?; + self.push(v); + return Ok(()); + } + let n = self.builtin_len(o)?; + let v = self.int_to_val(Some(n))?; + self.push(v); Ok(()) + } + + fn call_str(&mut self, argc: u16, chunk: &Self::Code) -> Result<(), VmErr> { + let ok = self.take_job_ok(); + if argc == 0 { return self.alloc_and_push_str(alloc::string::String::new()); } + if argc >= 2 { + // `str(bytes, encoding[, errors])` decodes, mirroring `bytes.decode`. + let args = self.pop_n(argc as usize)?; + for (&a, name) in args[1..].iter().zip(["encoding", "errors"]) { + if !matches!(self.heap().try_get(a), Some(HeapObj::Str(_))) { + return Err(VmErr::TypeMsg(crate::s!("str() argument '", str name, "' must be str, not ", str self.type_name(a)))); + } + } + if self.heap().bytes_of(args[0]).is_some() { + return crate::vm::methods::bytes::decode(self, args[0], &args[1..]); + } + return Err(VmErr::TypeMsg(crate::s!("decoding to str: need a bytes-like object, ", str self.type_name(args[0]), " found"))); + } + let o = self.pop()?; + // A container writes its items with repr, as `repr` does, and an instance its `__str__` or `__repr__`, each run as frames of the loop. + if ok && o.is_heap() && self.render_job(o, false, chunk)? { return Ok(()); } + if ok && self.is_instance(o) && self.str_job(o, false) { return Ok(()); } + let s = self.display_op(o, chunk)?; + self.alloc_and_push_str(s) + } + + fn call_bool(&mut self, argc: u16, chunk: &Self::Code) -> Result<(), VmErr> { + let ok = self.take_job_ok(); + if argc == 0 { self.push(Val::bool(false)); return Ok(()); } + let o = self.pop()?; + // An instance's `__bool__`, or past it its `__len__`, runs as a frame of the loop. + if ok && self.is_instance(o) { + let class = self.instance_class(o).unwrap_or(Val::undef()); + let has_bool = self.lookup_class_member(class, "__bool__").is_some(); + if if has_bool { self.check_job(o, crate::vm::jobs::Check::Bool) } else { self.check_job(o, crate::vm::jobs::Check::LenTruth) } { return Ok(()); } + } + let t = self.truthy_op(o, chunk)?; + self.push(Val::bool(t)); + Ok(()) + } + + fn call_repr(&mut self, chunk: &Self::Code) -> Result<(), VmErr> { + let ok = self.take_job_ok(); + let o = self.pop()?; + // A container of instances whose `__repr__` runs as frames of the loop writes there, as an instance's own does. + if ok && o.is_heap() && self.render_job(o, false, chunk)? { return Ok(()); } + if ok && self.is_instance(o) && self.check_job(o, crate::vm::jobs::Check::Str(true)) { return Ok(()); } + let s = self.repr_op(o, chunk)?; + self.alloc_and_push_str(s) + } + + /* `ascii(x)`, the repr of `x` with every non-ASCII character escaped. */ + fn call_ascii(&mut self, chunk: &Self::Code) -> Result<(), VmErr> { + let o = self.pop()?; + let s = self.repr_op(o, chunk)?; + self.alloc_and_push_str(crate::vm::format_spec::ascii_escape(&s)) + } + + fn call_callable(&mut self) -> Result<(), VmErr> { + let o = self.pop()?; + let result = self.is_callable(o); + self.push(Val::bool(result)); + Ok(()) + } + + /* Whether `o` can be called, an instance or a value a class made only when its class defines `__call__`. */ + fn is_callable(&self, o: Val) -> bool { + if let Some(cls) = self.instance_class(o) { return self.lookup_class_member(cls, "__call__").is_some(); } + matches!(self.heap().try_get(o), Some(HeapObj::Func(..) | HeapObj::BoundMethod(..) | HeapObj::Type(_) | HeapObj::NativeFn(_) + | HeapObj::Class(..) | HeapObj::BoundUserMethod(..) | HeapObj::Extern(_) | HeapObj::PropertyPart(..) + | HeapObj::StaticMethod(_) | HeapObj::GenericAlias(..))) + } + + /* The name `type(x)` reports, a user instance's own class, an exception's concrete class, else the builtin type name. */ + fn type_repr_name(&self, o: Val) -> alloc::string::String { + if o.is_heap() { + match self.heap().get(o) { + // Exception instances report their concrete class (e.g. `ZeroDivisionError`). + HeapObj::ExcInstance(n, ..) => return n.clone(), + _ => if let Some(cls) = self.instance_class(o) && let HeapObj::Class(n, _, _) = self.heap().get(cls) { + return n.clone(); + }, + } + } + self.type_name(o).to_string() // interned, so shares the `set`/`int` singleton + } + + // `format(value [, spec])`. + fn call_format(&mut self, op: u16, chunk: &Self::Code) -> Result<(), VmErr> { + if op != 1 && op != 2 { + return Err(cold_type("format() takes 1 or 2 arguments")); + } + let spec_val = if op == 2 { Some(self.pop()?) } else { None }; + let val = self.pop()?; + let spec = match spec_val.map(|sv| self.heap().try_get(sv)) { + None => String::new(), + Some(Some(HeapObj::Str(s))) => s.clone(), + Some(_) => return Err(VmErr::TypeMsg(crate::s!("format() argument 2 must be str, not ", str self.type_name(spec_val.unwrap_or_else(Val::none))))), + }; + let result = self.format_op(val, &spec, chunk)?; + self.alloc_and_push_str(result) + } + + // `slice(stop)` | `slice(start, stop)` | `slice(start, stop, step)`, builtin, usable as a sequence index. + fn call_slice(&mut self, argc: u16) -> Result<(), VmErr> { + let args = self.pop_n(argc as usize)?; + let (start, stop, step) = match args.as_slice() { + [stop] => (Val::none(), *stop, Val::none()), + [start, stop] => (*start, *stop, Val::none()), + [start, stop, step] => (*start, *stop, *step), + _ => return Err(cold_type("slice() takes 1 to 3 arguments")), + }; + let v = self.heap_mut().alloc(HeapObj::Slice(start, stop, step))?; + self.push(v); Ok(()) + } + + /* `bytes_fromhex(s)`, the `bytes.fromhex` classmethod as a function. */ + fn call_bytes_fromhex(&mut self) -> Result<(), VmErr> { + let s = self.pop()?; + crate::vm::methods::bytes::fromhex(self, Val::none(), &[s]) + } + + /* `int_from_bytes(b, byteorder)`, the `int.from_bytes` classmethod as a function. */ + fn call_int_from_bytes(&mut self) -> Result<(), VmErr> { + let order = self.pop()?; + let b = self.pop()?; + if !matches!(self.heap().try_get(b), Some(HeapObj::Bytes(_))) { return Err(cold_type("int_from_bytes() first arg must be bytes")); } + crate::vm::methods::numeric::from_bytes(self, Val::none(), &[b, order]) + } + + // `int_to_bytes(n, length, byteorder)`, the `int.to_bytes` method as a function. + fn call_int_to_bytes(&mut self) -> Result<(), VmErr> { + let args = self.pop_n(3)?; + crate::vm::methods::numeric::to_bytes(self, args[0], &args[1..]) + } + + /* Coerce a `__len__` / `__length_hint__` return value to bool semantics and reject negatives. */ + fn len_to_bool(&self, v: Val) -> Result { + let n = if v.is_int() { v.as_int() as i128 } + else if let Some(i) = crate::vm::types::as_i128(v, self.heap()) { i } + else { return Err(cold_type("__len__ must return int")); }; + if n < 0 { return Err(cold_value("__len__() should return >= 0")); } + Ok(n != 0) + } + + // Materialise an iterable to a list, strings -> chars, ranges eager, coroutines drained. + fn call_list(&mut self, argc: u16, chunk: &Self::Code) -> Result<(), VmErr> { + let ok = self.take_job_ok(); + if argc == 0 { return self.alloc_and_push_list(Vec::new()); } // `list()` is the empty list. + let o = self.pop()?; + // A generator, or a user iterator's `__next__`, called where a frame may open runs as a frame of the loop, its items gathered as they come. + if ok && self.may_job(o) && self.items_job(o, crate::vm::jobs::Takes::All(OpCode::CallList, 1), chunk)? { return Ok(()); } + // user-defined iterable wins over the built-in dispatch. + if let Some(items) = self.iter_to_vec_op(o, chunk)? { + return self.alloc_and_push_list(items); + } + let items = self.extract_iter(o)?; + self.alloc_and_push_list(items) + } + + fn call_tuple(&mut self, argc: u16, chunk: &Self::Code) -> Result<(), VmErr> { + let ok = self.take_job_ok(); + if argc == 0 { return self.alloc_and_push_tuple(Vec::new()); } // `tuple()` is the empty tuple. + let o = self.pop()?; + if ok && self.may_job(o) && self.items_job(o, crate::vm::jobs::Takes::All(OpCode::CallTuple, 1), chunk)? { return Ok(()); } + if let Some(items) = self.iter_to_vec_op(o, chunk)? { + return self.alloc_and_push_tuple(items); + } + let items = self.extract_iter(o)?; + self.alloc_and_push_tuple(items) + } + + // Operand packs kw<<8 | pos, keep counts distinct. + fn call_dict(&mut self, op: u16) -> Result<(), VmErr> { + let pos = (op & 0xFF) as usize; + let kw = (op >> 8) as usize; + if pos > 1 { + return Err(cold_type("dict expected at most 1 argument")); + } + // Keyword pairs sit above the positional source. + let ok = self.take_job_ok(); + let kw_flat = self.pop_n(kw * 2)?; + let src = if pos == 1 { Some(self.pop()?) } else { None }; + // A generator called where a frame may open runs as a frame of the loop, its pairs gathered as it yields them. + if ok && kw == 0 && let Some(s) = src && s.is_heap() && self.gather_gen(s, crate::vm::jobs::Takes::All(crate::parser::OpCode::CallDict, op), &[])? { return Ok(()); } + // A dict source copies with the hashes it stored. + if let Some(rc) = src.and_then(|s| self.heap().dict_cell(s)) && kw_flat.is_empty() { + let dm = rc.borrow().clone(); + return self.alloc_and_push_dict(dm); + } + let mut pairs = match src { Some(s) => self.pairs_of(s)?, None => Vec::new() }; + pairs.extend(kw_flat.chunks(2).map(|p| (p[0], p[1]))); + let dm = self.dictmap_of(pairs)?; + self.alloc_and_push_dict(dm) + } + + /* `frozendict()` as `dict()` builds it, a frozendict alone given back as itself. */ + fn call_frozendict(&mut self, op: u16) -> Result<(), VmErr> { + if op == 1 && let Some(top) = self.last() && matches!(self.heap().try_get(top), Some(HeapObj::FrozenDict(_))) { return Ok(()); } + self.call_dict(op)?; + let made = self.last().ok_or_else(|| cold_runtime("stack underflow"))?; + if let Some(slot) = self.heap_mut().try_get_mut(made) && let HeapObj::Dict(rc) = slot { *slot = HeapObj::FrozenDict(rc.clone()); } + Ok(()) + } + + fn call_set(&mut self, op: u16) -> Result<(), VmErr> { + let ok = self.take_job_ok(); + let src = if op == 0 { None } else { Some(self.pop()?) }; + if ok && let Some(s) = src && s.is_heap() && self.gather_gen(s, crate::vm::jobs::Takes::All(crate::parser::OpCode::CallSet, 1), &[])? { return Ok(()); } + let s = self.set_source(src)?; + let val = self.heap_mut().alloc(HeapObj::Set(Rc::new(RefCell::new(s))))?; + self.push(val); + Ok(()) + } + + /* The items `set(src)` or `frozenset(src)` holds, a set source copies the hashes it stored. */ + fn set_source(&mut self, src: Option) -> Result { + let Some(src) = src else { return Ok(ValSet::new()) }; + match self.heap().try_get(src) { + Some(HeapObj::Set(rc)) => return Ok(rc.borrow().clone()), + Some(HeapObj::FrozenSet(rc)) => return Ok((**rc).clone()), + _ => {} + } + let items = self.extract_iter(src)?; + self.valset_of(&items) + } + + /* `frozenset()` | `frozenset(iter)`, construct an immutable, hashable set from an iterable. Without args returns the empty frozenset. */ + fn call_frozenset(&mut self, argc: u16) -> Result<(), VmErr> { + let args = self.pop_n(argc as usize)?; + if args.len() > 1 { return Err(cold_type("frozenset() takes 0 or 1 argument")); } + let s = self.set_source(args.first().copied())?; + let v = self.heap_mut().alloc(HeapObj::FrozenSet(Rc::new(s)))?; + self.push(v); Ok(()) + } + + /* `bytes()`, empty, `n` zero bytes, an iterable of ints in range(256), a bytes-like value or `(str, encoding, errors)`. */ + #[inline(never)] + fn call_bytes(&mut self, argc: u16) -> Result<(), VmErr> { + let args = self.pop_n(argc as usize)?; + let buf = self.octets_of(&args, "bytes")?; + let v = self.heap_mut().alloc(HeapObj::Bytes(buf))?; + self.push(v); Ok(()) + } + + /* `bytearray(...)`, the arguments `bytes()` takes, a mutable copy. */ + fn call_bytearray(&mut self, argc: u16) -> Result<(), VmErr> { + let args = self.pop_n(argc as usize)?; + let buf = self.octets_of(&args, "bytearray")?; + let v = self.heap_mut().alloc(HeapObj::ByteArray(buf))?; + self.push(v); Ok(()) + } + + /* The octets the arguments of `bytes()` or `bytearray()` name. */ + fn octets_of(&mut self, args: &[Val], what: &str) -> Result, VmErr> { + Ok(match *args { + [] => Vec::new(), + [a] if a.is_int() || a.is_bool() || crate::frontend::int::is_big(self.heap(), a) => { + let n = self.as_i128(a).unwrap_or(i128::MAX); + if n < 0 { return Err(cold_value("negative count")); } + // Length is user-controlled, cap it against the memory left so a huge count errors instead of aborting. + let n = usize::try_from(n).map_err(|_| cold_overflow())?; + self.heap().reserve(n)?; + alloc::vec![0u8; n] + } + [a] if self.heap().bytes_of(a).is_some() => self.heap().bytes_of(a).unwrap_or_default().to_vec(), + [a] if matches!(self.heap().try_get(a), Some(HeapObj::Str(_))) => return Err(VmErr::TypeMsg(crate::s!("string argument without an encoding"))), + [a] => { + let items = self.extract_iter(a).map_err(|_| VmErr::TypeMsg(crate::s!("cannot convert '", str self.type_name(a), "' object to ", str what)))?; + // `bytes` names its range in the plural, `bytearray` in the singular. + items.into_iter().map(|v| self.byte_value(v).map_err(|e| if what == "bytes" && matches!(e, VmErr::Value(_)) { cold_value("bytes must be in range(0, 256)") } else { e })).collect::, VmErr>>()? + } + [text, ref rest @ ..] if rest.len() <= 2 => { + for (&a, name) in rest.iter().zip(["encoding", "errors"]) { + if !matches!(self.heap().try_get(a), Some(HeapObj::Str(_))) { + return Err(VmErr::TypeMsg(crate::s!(str what, "() argument '", str name, "' must be str, not ", str self.type_name(a)))); + } + } + let Some(HeapObj::Str(t)) = self.heap().try_get(text) else { return Err(cold_type("encoding without a string argument")) }; + let (encoding, errors) = crate::vm::methods::string::codec_args(self, rest)?; + crate::vm::codecs::encode(t, encoding, errors)? + } + _ => return Err(VmErr::TypeMsg(crate::s!(str what, "() takes at most 3 arguments"))), + }) + } + + /* Entries of a mapping, or of an iterable of two-item iterables, as `dict()` takes them. */ + fn pairs_of(&mut self, src: Val) -> Result, VmErr> { + if let Some(rc) = self.heap().dict_cell(src) { self.heap().spend(rc.borrow().len()); return Ok(rc.borrow().iter().collect()); } + let mut pairs = Vec::new(); + for item in self.extract_iter(src)? { + let item = self.extract_iter(item).map_err(|e| match e { + VmErr::TypeMsg(_) => VmErr::TypeMsg("object is not iterable".into()), + e => e, + })?; + let [k, v] = item[..] else { + return Err(VmErr::Raised(crate::s!("ValueError: dictionary update sequence element #", int pairs.len() as i64, " has length ", int item.len() as i64, "; 2 is required"))); + }; + self.require_key(k)?; + self.keyed(k)?; + pairs.push((k, v)); + } + Ok(pairs) + } + + /* An iterator over `o`, the one `iter(o)` gives. */ + fn iter_of(&mut self, o: Val, chunk: &Self::Code) -> Result { + let ascii = self.heap_mut().str_is_ascii(o); + if self.heap().classed(o).is_some() && let Some(it) = self.try_call_dunder(o, "__iter__", &[], chunk)? { return Ok(it); } + let name = match self.heap().try_get(o) { + Some(HeapObj::Coroutine(..)) if self.is_async_object(o) => return Err(self.not_iterable(o)), + Some(HeapObj::Iter(..) | HeapObj::Coroutine(..)) => return Ok(o), + Some(HeapObj::Instance(..)) => return self.try_call_dunder(o, "__iter__", &[], chunk)?.ok_or_else(|| self.not_iterable(o)), + Some(HeapObj::List(rc)) => { let rc = rc.clone(); return self.alloc_iterator(IterFrame::List { rc, idx: 0 }, "list_iterator"); } + Some(&HeapObj::Range(cur, end, step)) => return self.alloc_iterator(IterFrame::Range { cur, end, step }, "range_iterator"), + Some(&HeapObj::BigRange(cur, end, step)) => return self.alloc_iterator(IterFrame::BigRange { cur, end, step }, "range_iterator"), + Some(HeapObj::Dict(_) | HeapObj::FrozenDict(_)) => return self.alloc_iterator(self.dict_walk(o, View::Keys), "dict_keyiterator"), + Some(&HeapObj::DictView(d, kind)) => { + let name = match kind { View::Keys => "dict_keyiterator", View::Values => "dict_valueiterator", View::Items => "dict_itemiterator" }; + return self.alloc_iterator(self.dict_walk(d, kind), name); + } + Some(HeapObj::Tuple(_)) => "tuple_iterator", + Some(HeapObj::Str(_)) => if ascii { "str_ascii_iterator" } else { "str_iterator" }, + Some(HeapObj::Set(_) | HeapObj::FrozenSet(_)) => "set_iterator", + Some(HeapObj::Bytes(_)) => "bytes_iterator", + Some(HeapObj::ByteArray(_)) => "bytearray_iterator", + Some(HeapObj::MemoryView(_)) => "memory_iterator", + _ => return Err(self.not_iterable(o)), + }; + let items = self.extract_iter(o)?; + let frame = self.watched(o, items); + self.alloc_iterator(frame, name) + } + + /* Turns the stack values from `at` into iterators in place, each rooted while the next is made. */ + fn iters_in_place(&mut self, at: usize, chunk: &Self::Code) -> Result<(), VmErr> { + for i in at..self.stack().len() { + let v = self.stack()[i]; + let it = self.iter_of(v, chunk)?; + self.stack()[i] = it; + } + Ok(()) + } + + fn call_enumerate(&mut self, op: u16, chunk: &Self::Code) -> Result<(), VmErr> { + let (positional, kw_flat) = self.parse_call_args(op)?; + if positional.is_empty() || positional.len() > 2 { + return Err(cold_type("enumerate() takes 1 or 2 positional arguments")); + } + // `start` is positional (`enumerate(xs, 5)`) or keyword (`enumerate(xs, start=5)`), default 0. + let mut start = if positional.len() == 2 { positional[1] } else { Val::int(0) }; + for pair in kw_flat.as_chunks::<2>().0 { + match self.kw_name(pair[0]) { + Some("start") => start = pair[1], + _ => return Err(cold_type("enumerate() got an unexpected keyword argument")), + } + } + if self.as_i128(start).is_none() { return Err(cold_type("enumerate() start must be an integer")); } + let it = self.with_roots([positional[0], start], |vm| vm.iter_of(positional[0], chunk))?; + self.push_iterator(IterFrame::Enumerate { it, n: start }, "enumerate") + } + + /* Pairs the items of N iterables into tuples as they are asked for, stopping at the shortest. */ + fn call_zip(&mut self, op: u16, chunk: &Self::Code) -> Result<(), VmErr> { + // `strict=` is the one keyword, its pair above the iterables. + let (pos, kw) = ((op & 0xFF) as usize, (op >> 8) as usize); + let mut strict = false; + for pair in self.pop_n(2 * kw)?.chunks(2) { + if self.kw_name(pair[0]) != Some("strict") { + return Err(VmErr::TypeMsg(s!("zip() got an unexpected keyword argument '", str self.kw_name(pair[0]).unwrap_or(""), "'"))); + } + strict = self.truthy(pair[1]); + } + let at = self.stack().len().checked_sub(pos).ok_or_else(|| cold_runtime("stack underflow"))?; + self.iters_in_place(at, chunk)?; + let its = self.pop_n(pos)?; + self.push_iterator(IterFrame::Zip { its: its.into(), strict }, "zip") + } + + /* `reversed(seq)`, a list read live from its end, a range stepped backwards, other sequences read up front. */ + #[inline(never)] + fn call_reversed(&mut self) -> Result<(), VmErr> { + let o = self.pop()?; + let name = match self.heap().try_get(o) { + Some(HeapObj::List(rc)) => { + let (rc, idx) = (rc.clone(), rc.borrow().len()); + return self.push_iterator(IterFrame::ListRev { rc, idx }, "list_reverseiterator"); + } + Some(&HeapObj::Range(start, end, step)) => { + let n = crate::vm::eq::range_len(start, end, step); + let (last, stop) = ((start as i128) + (n - 1) * step as i128, start as i128 - step as i128); + // A range whose backward bounds leave i64 reads its items up front. + if let (true, Some(back), Ok(cur), Ok(end)) = (n > 0, step.checked_neg(), i64::try_from(last), i64::try_from(stop)) { + return self.push_iterator(IterFrame::Range { cur, end, step: back }, "range_iterator"); + } + "range_iterator" + } + Some(HeapObj::BigRange(..)) => { + let (cur, end, step) = crate::frontend::int::range_reversed(self.heap_mut(), o).unwrap_or(Err(VmErr::Overflow))?; + return self.push_iterator(IterFrame::BigRange { cur, end, step }, "range_iterator"); + } + Some(HeapObj::Str(_) | HeapObj::Tuple(_) | HeapObj::Bytes(_) | HeapObj::ByteArray(_)) => "reversed", + Some(HeapObj::Dict(_) | HeapObj::FrozenDict(_) | HeapObj::DictView(_, View::Keys)) => "dict_reversekeyiterator", + Some(HeapObj::DictView(_, View::Values)) => "dict_reversevalueiterator", + Some(HeapObj::DictView(_, View::Items)) => "dict_reverseitemiterator", + _ => return Err(VmErr::TypeMsg(s!("'", str self.type_name(o), "' object is not reversible"))), + }; + let mut items = self.extract_iter(o)?; + items.reverse(); + let frame = self.watched(o, items); + self.push_iterator(frame, name) + } + + fn call_divmod(&mut self) -> Result<(), VmErr> { + let b = self.pop()?; + let a = self.pop()?; + let q = self.floor_op(BinOp::FloorDiv, a, b, "divmod()")?; + let r = self.floor_op(BinOp::Mod, a, b, "divmod()")?; + self.alloc_and_push_tuple(alloc::vec![q, r]) + } + + #[inline(never)] + fn call_pow(&mut self, op: u16) -> Result<(), VmErr> { + let args = self.pop_n(op as usize)?; + match args.len() { + 2 => { + let r = self.pow_vals(args[0], args[1], "pow() requires numeric operands")?; + self.push(r); + Ok(()) + } + 3 => { + // A modulus under 2^63 keeps every product in i128, so small operands skip the boxed ints. + if let (Some(b), Some(e), Some(m)) = (self.as_i128(args[0]), self.as_i128(args[1]), self.as_i128(args[2])) + && m != 0 && m.unsigned_abs() < 1 << 63 { + let (am, mut acc, mut b, mut e) = (m.abs(), 1i128, b, e); + b = b.rem_euclid(am); + // A negative exponent raises the inverse of the base, which needs it coprime to the modulus. + if e < 0 { + let (mut r, mut r1, mut t, mut t1) = (am, b, 0i128, 1i128); + while r1 != 0 { let q = r / r1; (r, r1, t, t1) = (r1, r - q * r1, t1, t - q * t1); } + if r != 1 { return Err(cold_value("base is not invertible for the given modulus")); } + (b, e) = (t.rem_euclid(am), -e); + } + while e > 0 { if e & 1 == 1 { acc = acc * b % am; } b = b * b % am; e >>= 1; } + let r = acc % am; + let r = self.heap_mut().int(if m < 0 && r != 0 { r - am } else { r })?; + self.push(r); + return Ok(()); + } + let r = crate::frontend::int::pow_mod(self.heap_mut(), args[0], args[1], args[2]) + .ok_or_else(|| cold_type("pow() 3rd argument not allowed unless all arguments are integers"))??; + self.push(r); + Ok(()) + } + _ => Err(cold_type("pow() takes 2 or 3 arguments")), + } + } + + // Folds plain items here and leaves the rest of the fold in `rest`. + fn fold_ready(&mut self, o: Val, takes: Takes, extra: &[Val], chunk: &Self::Code, rest: &mut Option) -> Result<(), VmErr> { + // An iterator steps as asked, so the fold stops at the item that decides it, and a container is read through its cursor. + let lazy = matches!(self.heap().try_get(o), Some(HeapObj::Iter(..) | HeapObj::Coroutine(..) | HeapObj::Instance(..))); + let (co, mut cur) = if lazy { (self.iter_of(o, chunk)?, None) } else { (o, Some(self.iter_cursor(o)?)) }; + let mut j = self.gather_of(takes, extra.to_vec(), co); + let (mut first, mut held) = (None, None); + if let Some(c) = cur.as_mut() { + // Ints of a machine word add with no look at the job. + if let (Takes::Sum, Total::Word(w)) = (j.takes, j.total) { + let (heap, budget) = self.heap_budget(); + let (total, stop) = sum_words(c, w, heap, budget); + (j.total, held) = (Total::Word(total), stop); + } + while first.is_none() { + let next = match held.take() { Some(v) => Some(v), None => c.next_item(self.heap_mut())? }; + match next { + None => { let r = self.fold_end(&mut j)?; self.push(r); return Ok(()); } + Some(v) if *self.budget_mut() != 0 && !self.heap().has_work() && take_plain(&mut j, v, self.heap()) => *self.budget_mut() -= 1, + // An item that is no instance, the best or total no instance either, takes no user code either. + Some(v) if !self.is_instance(v) && !self.is_instance(j.acc) && match j.takes { + // Two lists or tuples whose walk meets an instance compare through its dunders. + Takes::Extreme(_) => !matches!(self.pair_scan(v, j.acc, 0), Some(PairScan::Dunder(_))), + Takes::Sum | Takes::Any | Takes::Every => true, + Takes::Keyed(_, k) => matches!(self.heap().try_get(k), Some(HeapObj::NativeFn(_))), + _ => false, + } => { + self.charge_step()?; + match j.takes { + Takes::Sum => (j.total, j.acc) = self.sum_on(j.total, j.acc, v, chunk)?, + Takes::Extreme(is_min) => if j.acc.is_undef() || self.beats(v, j.acc, is_min, chunk)? { j.acc = v; }, + // A builtin key runs at once, the keys it gives held while any `__lt__` of theirs compares them. + Takes::Keyed(is_min, k) => { + self.push(k); + self.push(v); + self.exec_call(1, chunk)?; + let kv = self.pop()?; + let best = j.best_key; + if best.is_undef() || self.with_roots([kv, best, j.acc, v], |vm| vm.beats(kv, best, is_min, chunk))? { (j.acc, j.best_key) = (v, kv); } + } + t => if self.truthy(v) == (t == Takes::Any) { self.push(Val::bool(t == Takes::Any)); return Ok(()); }, + } + } + Some(v) => first = Some(v), + } + } + let left = *self.budget_mut(); + self.heap().set_budget(left); + } + *rest = Some(Folded { j, cur, first, lazy, co }); + Ok(()) + } + + /* The result of the builtin of `j` once its generator returned `ret`, its items taken out of it. */ + // Total, best or default once the items end. + fn fold_end(&mut self, j: &mut Gather) -> Result { + match j.takes { + Takes::Any => Ok(Val::bool(false)), + Takes::Every => Ok(Val::bool(true)), + Takes::Sum => Ok(match j.total { + Total::Word(w) => return self.int_to_val(Some(w as i128)), + Total::Float(s, c) => Val::float(super::globals::numeric::settle((s, c))), + Total::Any => j.acc, + }), + Takes::Extreme(is_min) | Takes::Keyed(is_min, _) if j.acc.is_undef() => match j.extra.first() { + // A `default` given is the result of an empty iterable. + Some(&d) => Ok(d), + None => Err(crate::vm::cold_value(if is_min { "min() iterable argument is empty" } else { "max() iterable argument is empty" })), + }, + Takes::Extreme(_) | Takes::Keyed(..) => Ok(j.acc), + _ => Err(cold_runtime("a gather that is no fold")), + } + } + + /* A job for the builtin `takes` stands for over `of`, a generator or a user iterator, `extra` its arguments after. */ + fn gather_of(&self, takes: Takes, extra: Vec, of: Val) -> Gather { + let mut j = Gather::new(takes, extra); + // What the items come from stays held while user code the builtin runs between them collects. + j.co = of; + if takes == Takes::Sum { + // The start the items add to, 0 unless passed. + let start = j.extra.first().copied().unwrap_or(Val::int(0)); + let word = if start.is_int() || start.is_foreign() || start.is_bool() { self.as_i128(start).and_then(|n| i64::try_from(n).ok()) } else { None }; + j.total = match word { Some(w) => Total::Word(w), None if start.is_float() => Total::Float(start.as_float(), 0.0), None => Total::Any }; + j.acc = start; + } + j + } + + /* The total of a `sum` that went as far as `total`, `acc` its value past the floats, once `v` adds to it. */ + fn sum_on(&mut self, total: Total, acc: Val, v: Val, chunk: &Self::Code) -> Result<(Total, Val), VmErr> { + let word = |vm: &Self, v: Val| if v.is_int() || v.is_bool() || v.is_foreign() { vm.as_i128(v).and_then(|n| i64::try_from(n).ok()) } else { None }; + let (total, acc) = match total { + Total::Word(w) => match word(self, v).and_then(|b| w.checked_add(b)) { + Some(n) => (Total::Word(n), acc), + None => { + let a = self.int_to_val(Some(w as i128))?; + let a = self.sum_through(a, v, chunk)?; + if a.is_float() { (Total::Float(a.as_float(), 0.0), a) } else { (Total::Any, a) } + } + }, + Total::Float(s, c) if v.is_float() => { let (s, c) = super::globals::numeric::neumaier(s, c, v.as_float()); (Total::Float(s, c), acc) } + Total::Float(s, c) => match word(self, v) { + Some(b) => (Total::Float(s + b as f64, c), acc), + None => (Total::Any, self.sum_through(Val::float(super::globals::numeric::settle((s, c))), v, chunk)?), + }, + Total::Any => (Total::Any, self.sum_through(acc, v, chunk)?), + }; + Ok((total, acc)) + } + + /* `a + b` for `sum`, both rooted while a user `__add__` runs. */ + fn sum_through(&mut self, a: Val, b: Val, chunk: &Self::Code) -> Result { + self.with_roots([a, b], |vm| vm.sum_add(a, b, chunk)) + } + + /* One step of `sum`, through a user `__add__` or `__radd__` when either side defines one. */ + fn sum_add(&mut self, a: Val, b: Val, chunk: &Self::Code) -> Result { + match self.try_binary_dunder(crate::parser::types::OpCode::Add, a, b, false, chunk)? { + Some(v) => Ok(v), + None => self.add_vals(crate::value::unboxed(a, self.heap()), crate::value::unboxed(b, self.heap())), + } + } + + /* Whether `item < best` for min or `item > best` for max, raising with the operator that ran. */ + #[inline] + fn beats(&mut self, item: Val, best: Val, is_min: bool, chunk: &Self::Code) -> Result { + if item.is_int() && best.is_int() { return Ok(if is_min { item.as_int() < best.as_int() } else { item.as_int() > best.as_int() }); } + if is_min { return self.sort_lt(item, best, chunk); } + if let Some(r) = self.try_compare_dunder(crate::parser::OpCode::Gt, item, best, chunk)? { return Ok(self.truthy(r)); } + match self.values_lt(best, item, chunk) { + Err(VmErr::TypeMsg(_)) => Err(VmErr::TypeMsg(crate::s!("'>' not supported between instances of '", str self.type_name(item), "' and '", str self.type_name(best), "'"))), + r => r, + } + } + + /* Index of the least key, or with `!is_min` the greatest, the first one on a tie. */ + fn extreme_index(&mut self, keys: &[Val], is_min: bool, chunk: &Self::Code) -> Result { + let mut best = 0; + for i in 1..keys.len() { + if self.beats(keys[i], keys[best], is_min, chunk)? { best = i; } + } + Ok(best) + } + + fn call_min(&mut self, op: u16, chunk: &Self::Code) -> Result<(), VmErr> { self.call_minmax(op, true, chunk) } + + fn call_max(&mut self, op: u16, chunk: &Self::Code) -> Result<(), VmErr> { self.call_minmax(op, false, chunk) } + + fn call_minmax(&mut self, op: u16, is_min: bool, chunk: &Self::Code) -> Result<(), VmErr> { + let ok = self.take_job_ok(); + let (positional, kw_flat) = self.parse_call_args(op)?; + // Optional `default=` (returned when a single iterable is empty) and `key=` (compare by key(x)). + let mut default: Option = None; + let mut key: Option = None; + for pair in kw_flat.as_chunks::<2>().0 { + match self.kw_name(pair[0]) { + Some("default") => default = Some(pair[1]), + Some("key") => { if !pair[1].is_none() { key = Some(pair[1]); } } + _ => return Err(cold_type("min()/max() got an unexpected keyword argument")), + } + } + let label = if is_min { "min() iterable argument is empty" } else { "max() iterable argument is empty" }; + // An `eval` or `exec` key runs each text as a floor of the loop. + if ok && let Some(k) = key && self.runs_text(k) { + // An iterable that may run Python has its items gathered by a job of the loop first, the call run again on them. + if let [o] = positional[..] && matches!(self.heap().try_get(o), Some(HeapObj::Instance(..) | HeapObj::Coroutine(..) | HeapObj::Iter(..))) { + let base = self.stack().len(); + self.push(o); + for &v in &kw_flat { self.push(v); } + let id = if is_min { NativeFnId::Min } else { NativeFnId::Max }; + if self.gather_call(id, op, base, chunk)? { return Ok(()); } + let o = self.stack()[base]; + self.stack().truncate(base); + let items = self.extract_iter(o)?; + if items.is_empty() { return match default { Some(d) => { self.push(d); Ok(()) }, None => Err(cold_value(label)) }; } + return self.keys_floor(k, items, is_min); + } + let items = match positional[..] { [o] => self.extract_iter(o)?, _ => positional }; + if items.is_empty() { return match default { Some(d) => { self.push(d); Ok(()) }, None => Err(cold_value(label)) }; } + return self.keys_floor(k, items, is_min); + } + // A builtin key stepping a generator among the items in Rust runs through a function of the loop, as a job stepping it, any iterable's items unknown yet. + if ok && let Some(k) = key && let Some(held) = self.held_key(k, &positional)? { + key = Some(held); + // Values whose key now runs as frames fold as one iterable does. + if positional.len() > 1 { + let all = self.heap_mut().alloc(HeapObj::Tuple(positional))?; + return self.with_roots([all], |vm| vm.fold_run(all, crate::vm::jobs::Takes::Keyed(is_min, held), &[], ok, chunk)); + } + } + // One iterable folds, an item at a time, so a range never becomes a list. + if let [o] = positional[..] { + let takes = match key { Some(k) => crate::vm::jobs::Takes::Keyed(is_min, k), None => crate::vm::jobs::Takes::Extreme(is_min) }; + return self.fold_run(o, takes, default.as_slice(), ok, chunk); + } + // Many args are values. + let items = positional; + if items.is_empty() { + return match default { Some(d) => { self.push(d); Ok(()) }, None => Err(cold_value(label)) }; + } + // Values whose comparison or key may run user code fold as one iterable does, its dunders frames of the loop. + if self.heap().users && (items.iter().any(|v| v.is_heap()) || key.is_some_and(|k| self.heap().user(k))) { + let takes = match key { Some(k) => crate::vm::jobs::Takes::Keyed(is_min, k), None => crate::vm::jobs::Takes::Extreme(is_min) }; + let all = self.heap_mut().alloc(HeapObj::Tuple(items))?; + return self.with_roots([all], |vm| vm.fold_run(all, takes, &[], ok, chunk)); + } + // Without a key, compare elements directly, with one, compare key(x) but return the winning element. + let keys = match key { + Some(k) => Some(self.call_rows(k, core::slice::from_ref(&items), items.len(), chunk)?), + None => None, + }; + let keys = keys.as_deref().unwrap_or(&items); + // An instance key runs `__lt__` like `sorted`, so then the operands stay rooted. + let roots = if self.any_instance(keys) { items.len() + keys.len() } else { 0 }; + let best = self.with_roots(items.iter().chain(keys).copied().take(roots), |vm| vm.extreme_index(keys, is_min, chunk))?; + self.push(items[best]); Ok(()) + } + + fn call_sum(&mut self, op: u16, chunk: &Self::Code) -> Result<(), VmErr> { + let ok = self.take_job_ok(); + let args = self.pop_n(op as usize)?; + if args.is_empty() { return Err(cold_type("sum() requires at least 1 argument")); } + // Text and bytes join, which sum points to. + match args.get(1).and_then(|&start| self.heap().try_get(start)) { + Some(HeapObj::Str(_)) => return Err(cold_type("sum() can't sum strings [use ''.join(seq) instead]")), + Some(HeapObj::Bytes(_)) => return Err(cold_type("sum() can't sum bytes [use b''.join(seq) instead]")), + Some(HeapObj::ByteArray(_)) => return Err(cold_type("sum() can't sum bytearray [use b''.join(seq) instead]")), + _ => {} + } + // Each item adds as it comes, as CPython 3.15 adds it. + self.fold_run(args[0], crate::vm::jobs::Takes::Sum, &args[1..], ok, chunk) + } + + // a < b via __lt__ when either side defines it, else the built-in comparison. + #[inline] + fn sort_lt(&mut self, a: Val, b: Val, chunk: &Self::Code) -> Result { + if let Some(r) = self.try_compare_dunder(OpCode::Lt, a, b, chunk)? { + return Ok(self.truthy(r)); + } + self.values_lt(a, b, chunk) + } + + /* Steps `o` lazily when a range or list, so `all`, `any` and `sum` stop early. */ + #[inline(always)] + fn iter_cursor(&mut self, o: Val) -> Result { + Ok(match self.heap().try_get(o) { + Some(&HeapObj::Range(cur, end, step)) => IterFrame::Range { cur, end, step }, + Some(&HeapObj::BigRange(cur, end, step)) => IterFrame::BigRange { cur, end, step }, + Some(HeapObj::List(rc)) => IterFrame::List { rc: rc.clone(), idx: 0 }, + _ => IterFrame::Seq { items: self.extract_iter(o)?.into(), idx: 0 }, + }) + } + + /* Short-circuit truthiness scan shared by `all`/`any`, stopping at the first item whose truth is `find` and pushing it, `!find` once spent. */ + fn scan_truthy(&mut self, op: u16, find: bool, arity_err: &'static str, chunk: &Self::Code) -> Result<(), VmErr> { + let ok = self.take_job_ok(); + if op != 1 { return Err(cold_type(arity_err)); } + let o = self.pop()?; + self.fold_run(o, if find { crate::vm::jobs::Takes::Any } else { crate::vm::jobs::Takes::Every }, &[], ok, chunk) + } + + fn call_all(&mut self, op: u16, chunk: &Self::Code) -> Result<(), VmErr> { self.scan_truthy(op, false, "all() takes exactly 1 argument", chunk) } + + fn call_any(&mut self, op: u16, chunk: &Self::Code) -> Result<(), VmErr> { self.scan_truthy(op, true, "any() takes exactly 1 argument", chunk) } + + /* Walks the pairs of `a` and `b` from `from` as `PairScan` says, items read as they are now, None unless both are builtin lists or both builtin tuples. */ + fn pair_scan(&self, a: Val, b: Val, from: usize) -> Option { + let (xs, ys): (&[Val], &[Val]) = match (self.heap().plain(a)?, self.heap().plain(b)?) { + (HeapObj::Tuple(x), HeapObj::Tuple(y)) => (x, y), + (HeapObj::List(x), HeapObj::List(y)) => return Some(scan_pairs(&x.borrow(), &y.borrow(), from, self.heap())), + _ => return None, + }; + Some(scan_pairs(xs, ys, from, self.heap())) + } + /* `sorted` with no key, its `__lt__` run as frames of the loop when `ok`, called where one may open. */ + fn sorted_of(&mut self, reverse: bool, ok: bool, chunk: &Self::Code) -> Result<(), VmErr> { + let o = self.pop()?; + let items = self.extract_iter(o)?; + let mut items = if ok { match self.merge_job(items, reverse, Val::undef()) { Ok(()) => return Ok(()), Err(items) => items } } else { items }; + self.sort_by_lt(&mut items, reverse, chunk)?; + self.alloc_and_push_list(items) + } + + /* In-place sort dispatching `__lt__`, roots items since a comparison can run user code that GCs. */ + fn sort_by_lt(&mut self, items: &mut [Val], reverse: bool, chunk: &Self::Code) -> Result<(), VmErr> { + let snapshot = items.to_vec(); + let order = self.sorted_order(&snapshot, &[], reverse, chunk)?; + for (dst, &src) in order.iter().enumerate() { items[dst] = snapshot[src]; } + Ok(()) + } + + /* Root the operands (comparators can run GC-triggering user code), sort `keys` the way Python does through `sort_lt`, and return the index permutation. `extra_roots` keeps caller-only values (e.g. the items in a keyed sort) alive across comparisons. The first comparison error ends the sort. */ + fn sorted_order(&mut self, keys: &[Val], extra_roots: &[Val], reverse: bool, chunk: &Self::Code) -> Result, VmErr> { + if let Some(order) = super::globals::sequence::plain_order(keys, reverse, self.heap()) { return Ok(order); } + let mut rooted = 0; + // Only an instance key runs user code that could collect. + if self.any_instance(keys) { for &v in keys.iter().chain(extra_roots) { self.root(v); rooted += 1; } } + // A descending sort reverses, sorts ascending and reverses back, so equal keys keep their order. + let mut order: Vec = if reverse { (0..keys.len()).rev().collect() } else { (0..keys.len()).collect() }; + let sorted = crate::vm::timsort::sort(&mut order, &mut |x, y| self.sort_lt(keys[x], keys[y], chunk)); + if reverse { order.reverse(); } + let sort_err = sorted.err(); + self.unroot(rooted); + if let Some(e) = sort_err { return Err(e); } + Ok(order) + } + + // One zip step and a strict zip raises where lengths differ. + fn zip_step(&mut self, its: &[Val], strict: bool, chunk: &Self::Code) -> Result, VmErr> { + let base = self.stack().len(); + if its.is_empty() { return Ok(None); } + if !strict { + if !self.push_next_of(its, chunk)? { return Ok(None); } + } else { + // A strict zip ends only when every iterable ends together. + for (k, &it) in its.iter().enumerate() { + match self.next_of(it, chunk)? { + Some(v) => self.push(v), + None if k > 0 => { self.stack().truncate(base); return Err(zip_uneven(k, false)); } + None => { + for (j, &rest) in its.iter().enumerate().skip(1) { + if let Some(extra) = self.next_of(rest, chunk)? { let _ = extra; return Err(zip_uneven(j, true)); } + } + return Ok(None); + } + } + } + } + let items = self.stack().split_off(base); + self.heap_mut().alloc(HeapObj::Tuple(items)).map(Some) + } + + // One enumerate step moving the count on in `frame`. + fn enumerate_step(&mut self, frame: Option<&Rc>>, it: Val, n: Val, chunk: &Self::Code) -> Result, VmErr> { + let Some(item) = self.next_of(it, chunk)? else { return Ok(None) }; + self.push(item); + let next = self.int_to_val(self.as_i128(n).and_then(|i| i.checked_add(1)))?; + if let Some(frame) = frame && let IterFrame::Enumerate { n: count, .. } = &mut *frame.borrow_mut() { *count = next; } + let item = self.pop()?; + self.heap_mut().alloc(HeapObj::Tuple(alloc::vec![n, item])).map(Some) + } + /* Pushes the next item of each iterator, false with nothing pushed once one is spent. */ + fn push_next_of(&mut self, its: &[Val], chunk: &Self::Code) -> Result { + let base = self.stack().len(); + for &it in its { + match self.next_of(it, chunk)? { + Some(v) => self.push(v), + None => { self.stack().truncate(base); return Ok(false); } + } + } + Ok(true) + } +} + +impl Builtins for H {} + +/* The error of a strict zip whose argument `k` ran out, or ran on, against those before it. */ +#[cold] +fn zip_uneven(k: usize, longer: bool) -> VmErr { + let before = if k == 1 { alloc::string::String::from("argument 1") } else { s!("arguments 1-", int k as i64) }; + let (n, how) = (k as i64 + 1, if longer { " is longer than " } else { " is shorter than " }); + cold_value_s(s!("zip() argument ", int n, str how, str &before)) +} + +// Adds machine word ints from `c` and gives back the first other item. +#[inline(always)] +fn sum_words(c: &mut IterFrame, mut w: i64, heap: &HeapPool, budget: &mut u64) -> (i64, Option) { + let mut left = *budget; + let mut stop = None; + while left != 0 && let Some(v) = c.next_ready(heap) { + match v.is_int().then(|| w.checked_add(v.as_int())).flatten() { + Some(n) => { w = n; left -= 1; } + None => { stop = Some(v); break; } + } + } + *budget = left; + (w, stop) +} diff --git a/src/vm/object_model.rs b/src/vm/object_model.rs index c9bc7d87..8018df27 100644 --- a/src/vm/object_model.rs +++ b/src/vm/object_model.rs @@ -6,6 +6,7 @@ use super::opcodes::attr_lookup::AttrLookup; use crate::parser::SSAChunk; use crate::s; use alloc::string::String; +use crate::vm::natives::Builtins; impl<'a> VM<'a> { /* `property 'x' of 'A' object has no setter`, a store or delete the property has no function for. */ diff --git a/src/vm/opcodes/arith.rs b/src/vm/opcodes/arith.rs index 65a1b795..f63acd66 100644 --- a/src/vm/opcodes/arith.rs +++ b/src/vm/opcodes/arith.rs @@ -193,21 +193,6 @@ impl<'a> VM<'a> { self.floor_op(BinOp::Mod, a, b, "%") } - /* `a // b` or `a % b`, floats when either side is one, signed like the divisor. */ - pub(crate) fn floor_op(&mut self, op: BinOp, a: Val, b: Val, sym: &str) -> Result { - let modulo = op == BinOp::Mod; - if a.is_float() || b.is_float() { - let (Some(af), Some(bf)) = (crate::vm::num_f64(a, &self.heap), crate::vm::num_f64(b, &self.heap)) else { return Err(self.unsupported(sym, a, b)); }; - let (af, bf) = (af?, bf?); - if bf == 0.0 { return Err(VmErr::ZeroDiv); } - let (q, r) = float_divmod(af, bf); - return Ok(Val::float(if modulo { r } else { q })); - } - if let (Some(x), Some(y)) = (self.as_i128(a), self.as_i128(b)) && y != 0 && let Some((q, r)) = crate::vm::int_divmod(x, y) { - return self.heap.int(if modulo { r } else { q }); - } - self.heap.foreign_binop(op, a, b)?.ok_or_else(|| self.unsupported(sym, a, b)) - } /* printf-style `str % args` translates each `%[flags][width][.prec]conv` into the `{:spec}` mini-language and reuses `format_value`. A tuple spreads, else one value. */ fn str_percent_format(&mut self, fmt_val: Val, arg: Val, chunk: &SSAChunk) -> Result { diff --git a/src/vm/opcodes/dunder.rs b/src/vm/opcodes/dunder.rs index e68028ce..6ad297a6 100644 --- a/src/vm/opcodes/dunder.rs +++ b/src/vm/opcodes/dunder.rs @@ -1,5 +1,4 @@ use super::*; -use crate::alloc::string::ToString; use crate::value::View; /* Single source of truth for opcode -> (forward, reflected) arithmetic dunder names. */ @@ -81,6 +80,7 @@ fn instance_class(heap: &HeapPool, v: Val) -> Option { match heap.get(v) { HeapObj::Instance(c, _) => Some(*c), _ => heap.classed(v).map(|e| e.0) } } + impl<'a> VM<'a> { /* `recv.(*args)` probes the instance method and invokes it with `self` prepended. `Some(v)` on return, `None` on miss / `NotImplemented` (triggers reflected/fallback dispatch), `Err` only on a raised dunder. */ pub(crate) fn try_call_dunder(&mut self, recv: Val, name: &str, args: &[Val], chunk: &SSAChunk) -> Result, VmErr> { @@ -173,9 +173,6 @@ impl<'a> VM<'a> { #[inline(always)] pub(crate) fn dunder_holds(&self, class: Val, a: Val, b: Val) -> bool { dunder_holds(&self.heap, class, a, b) } - /* Class of an Instance or of a dict or list a user class made, `None` for built-in operands. */ - #[inline] - pub(crate) fn instance_class(&self, v: Val) -> Option { instance_class(&self.heap, v) } /* Ordered forward/reflected dunder dispatch, reflected (`b.rname(a)`) runs first when `type(b)` strictly subclasses `type(a)` so overrides win. Returns the first non-None result. */ fn dispatch_reflected(&mut self, a: Val, b: Val, lname: &str, rname: &str, arith: bool, chunk: &SSAChunk) -> Result, VmErr> { @@ -242,24 +239,6 @@ impl<'a> VM<'a> { Ok(Some(r)) } - /* Python `bool()` semantics, try `__bool__`, then `__len__` (0 = False), else default True for instances. Pass-through for built-in types. */ - pub(crate) fn truthy_op(&mut self, v: Val, chunk: &SSAChunk) -> Result { - if self.instance_class(v).is_none() { - let t = self.truthy(v); - if t && v.is_heap() && self.heap.is_not_implemented(v) { return Err(cold_type("NotImplemented should not be used in a boolean context")); } - return Ok(t); - } - if let Some(r) = self.try_call_dunder(v, "__bool__", &[], chunk)? { - if !matches!(r, x if x.is_bool()) { - return Err(VmErr::TypeMsg(crate::s!("__bool__ should return bool, returned ", str self.type_name(r)))); - } - return Ok(r.as_bool()); - } - if let Some(r) = self.try_call_dunder(v, "__len__", &[], chunk)? { - return self.len_to_bool(r); - } - Ok(self.truthy(v)) - } /* Steps an iterator once, None once spent or when the value has no `__next__`. */ /* What a swallowed StopIteration carries, the pending exception set back to `before` the step raised it. */ @@ -440,98 +419,10 @@ impl<'a> VM<'a> { Ok(Some(self.heap.alloc(HeapObj::List(Rc::new(RefCell::new(items))))?)) } - /* `str(v)` semantics, instance `__str__` wins, then `__repr__`, else the built-in display. */ - pub(crate) fn display_op(&mut self, v: Val, chunk: &SSAChunk) -> Result { - if self.instance_class(v).is_some() { - if let Some(r) = self.try_call_dunder(v, "__str__", &[], chunk)? { - return self.require_str(r, "__str__"); - } - if let Some(r) = self.try_call_dunder(v, "__repr__", &[], chunk)? { - return self.require_str(r, "__repr__"); - } - } - // Containers render their elements with repr, dispatching user __repr__ on instances. - if self.is_container_val(v) { return self.repr_op(v, chunk); } - let s = match self.heap.foreign_repr(v) { Some(r) => r?, None => self.display(v) }; - // Render is O(size). Charge it so reprinting growing data can't outrun the budget. - self.charge_steps(s.len())?; - Ok(s) - } - /* `repr(v)` semantics, instance `__repr__` wins, otherwise the built-in repr (which adds quotes for strings, etc.). */ - pub(crate) fn repr_op(&mut self, v: Val, chunk: &SSAChunk) -> Result { - let s = self.repr_deep(v, chunk, &mut Vec::new())?; - self.charge_steps(s.len())?; - Ok(s) - } - fn is_container_val(&self, v: Val) -> bool { - v.is_heap() && matches!( - self.heap.get(v), - HeapObj::List(_) | HeapObj::Tuple(_) | HeapObj::Dict(_) | HeapObj::FrozenDict(_) | HeapObj::Set(_) | HeapObj::FrozenSet(_) - ) - } - /* Container-aware repr dispatches `__repr__` on nested instances. Elements always use repr, with `seen` tracking heap ids for cycle detection. */ - pub(crate) fn repr_deep(&mut self, v: Val, chunk: &SSAChunk, seen: &mut Vec) -> Result { - if let Some(r) = self.heap.foreign_repr(v) { return r; } - if !v.is_heap() { return Ok(self.repr(v)); } - if self.heap.classed(v).is_some() && let Some(r) = self.try_call_dunder(v, "__repr__", &[], chunk)? { - return self.require_str(r, "__repr__"); - } - if !self.is_container_val(v) { - if matches!(self.heap.get(v), HeapObj::Instance(..)) - && let Some(r) = self.try_call_dunder(v, "__repr__", &[], chunk)? { - return self.require_str(r, "__repr__"); - } - // The outer path goes along, so an exception inside its own args stops there. - return Ok(self.repr_d(v, seen)); - } - let id = v.as_heap(); - if seen.contains(&id) { - return Ok(match self.heap.get(v) { - HeapObj::Dict(_) | HeapObj::Set(_) => "{...}".into(), - HeapObj::FrozenDict(_) => "frozendict({...})".into(), - HeapObj::Tuple(_) => "(...)".into(), - HeapObj::FrozenSet(_) => "frozenset({...})".into(), - _ => "[...]".into(), - }); - } - if seen.len() > crate::vm::value_ops::RENDER_DEPTH_MAX { return Ok("...".into()); } - self.render_now(v, chunk, seen) - } - pub(crate) fn require_str(&self, v: Val, name: &str) -> Result { - if v.is_heap() && let HeapObj::Str(s) = self.heap.get(v) { return Ok(s.clone()); } - Err(VmErr::TypeMsg(crate::s!("'", str name, "' did not return a string"))) - } - /* `format(v, spec)`, an instance `__format__(spec)` wins, an empty spec is `str(v)` and the rest runs the spec engine. */ - pub(crate) fn format_op(&mut self, v: Val, spec: &str, chunk: &SSAChunk) -> Result { - if self.instance_class(v).is_some() { - let spec_val = self.heap.alloc(HeapObj::Str(spec.to_string()))?; - if let Some(r) = self.try_call_dunder(v, "__format__", &[spec_val], chunk)? { - if !matches!(self.heap.try_get(r), Some(HeapObj::Str(_))) { return Err(VmErr::TypeMsg(crate::s!("__format__ must return a str, not ", str self.type_name(r)))); } - return self.require_str(r, "__format__"); - } - } - let v = crate::value::unboxed(v, &self.heap); - if spec.is_empty() { return self.display_op(v, chunk); } - if let Some(z) = crate::frontend::complex::parts(&self.heap, v).filter(|_| crate::frontend::complex::is_complex(&self.heap, v)) { - return crate::vm::format_spec::format_complex(z.re, z.im, spec, &self.heap); - } - // Only numbers and strings read a spec, `f"{x!s:>9}"` pads anything else. - let own = v.is_int() || v.is_float() || v.is_bool() || crate::frontend::int::is_big(&self.heap, v) || matches!(self.heap.try_get(v), Some(HeapObj::Str(_))); - if !own { return Err(VmErr::TypeMsg(crate::s!("unsupported format string passed to ", str &self.type_repr_name(v), ".__format__"))); } - crate::vm::format_spec::format_typed(v, spec, self.type_name(v), &self.heap) - } - /* Coerce a `__len__` / `__length_hint__` return value to bool semantics and reject negatives. */ - pub(crate) fn len_to_bool(&self, v: Val) -> Result { - let n = if v.is_int() { v.as_int() as i128 } - else if let Some(i) = crate::vm::types::as_i128(v, &self.heap) { i } - else { return Err(cold_type("__len__ must return int")); }; - if n < 0 { return Err(cold_value("__len__() should return >= 0")); } - Ok(n != 0) - } } diff --git a/src/vm/opcodes/function.rs b/src/vm/opcodes/function.rs index a163f3bc..0f0dc686 100644 --- a/src/vm/opcodes/function.rs +++ b/src/vm/opcodes/function.rs @@ -26,7 +26,7 @@ fn iterates(id: super::super::types::NativeFnId) -> bool { /* A count outside a builtin arity, ranged ones name the bound they miss. */ #[cold] -fn arity_err(id: super::super::types::NativeFnId, n: u16) -> VmErr { +pub(crate) fn arity_err(id: super::super::types::NativeFnId, n: u16) -> VmErr { let (lo, hi) = id.arity(); let name = id.name(); // The name of a sentinel is its only positional, past two the count with `repr` is what is wrong. @@ -228,46 +228,14 @@ impl<'a> VM<'a> { _ => crate::vm::methods::dispatch_method(self, id, recv, pos, kw), }; done?; - match id.post() { - crate::vm::methods::Post::None => {} - crate::vm::methods::Post::ByteArray => self.top_as_bytearray()?, - // Only the built dict or set turns frozen, never a value the method returns. - crate::vm::methods::Post::Freeze => if let Some(&top) = self.stack.last() && let Some(slot) = self.heap.try_get_mut(top) { - match slot { - HeapObj::Dict(rc) => *slot = HeapObj::FrozenDict(rc.clone()), - HeapObj::Set(rc) => { let s = rc.borrow().clone(); *slot = HeapObj::FrozenSet(Rc::new(s)); } - _ => {} - } - }, + if id.post() != crate::vm::methods::Post::None && let Some(&top) = self.stack.last() { + let v = crate::vm::methods::post_result(self, id.post(), top)?; + if let Some(t) = self.stack.last_mut() { *t = v; } } self.pay_work() } - /* The value on top of the stack with every bytes in it, itself or the items of a list or tuple, made a bytearray. */ - #[inline(never)] - fn top_as_bytearray(&mut self) -> Result<(), VmErr> { - let Some(&top) = self.stack.last() else { return Ok(()) }; - let items = match self.heap.try_get(top) { - Some(HeapObj::Bytes(b)) => { let v = self.heap.alloc(HeapObj::ByteArray(b.clone()))?; *self.stack.last_mut().unwrap() = v; return Ok(()); } - Some(HeapObj::List(l)) => l.borrow().clone(), - Some(HeapObj::Tuple(t)) => t.clone(), - _ => return Ok(()), - }; - let mut out = Vec::with_capacity(items.len()); - for x in items { - out.push(match self.heap.try_get(x) { Some(HeapObj::Bytes(b)) => { let b = b.clone(); self.heap.alloc(HeapObj::ByteArray(b))? } _ => x }); - self.temp_roots.push(*out.last().unwrap()); - } - self.temp_roots.truncate(self.temp_roots.len() - out.len()); - match self.heap.try_get_mut(top) { - Some(HeapObj::List(l)) => *l.borrow_mut() = out, - Some(HeapObj::Tuple(t)) => *t = out, - _ => {} - } - Ok(()) - } - /* `str.lower(s)`, an unbound builtin method takes a receiver of its own type as the first argument. */ pub(crate) fn exec_unbound_method(&mut self, id: crate::vm::methods::BuiltinMethodId, args: &[Val], kw: &[Val], chunk: &SSAChunk) -> Result<(), VmErr> { let Some((&recv, rest)) = args.split_first() else { diff --git a/src/vm/opcodes/mod.rs b/src/vm/opcodes/mod.rs index 498f9046..b4d1399c 100644 --- a/src/vm/opcodes/mod.rs +++ b/src/vm/opcodes/mod.rs @@ -7,9 +7,10 @@ pub(crate) mod subscript; pub(super) use crate::vm::{ VM, Val, VmErr, HeapObj, HeapPool, DictMap, cache, - types::{BodyRef, ParkedFrame, VAL_BYTES, cold_depth, cold_type, cold_value, cold_value_s, cold_runtime, eq_member, eq_checked, float_divmod} + types::{Host, BodyRef, ParkedFrame, VAL_BYTES, cold_depth, cold_type, cold_value, cold_value_s, cold_runtime, eq_member, eq_checked} }; pub(super) use crate::parser::{OpCode, SSAChunk}; +pub(super) use crate::vm::natives::Builtins; pub(super) use alloc::{rc::Rc, string::String, vec, vec::Vec}; pub(super) use core::cell::RefCell; diff --git a/src/vm/opcodes/subscript.rs b/src/vm/opcodes/subscript.rs index e64a3f98..54893ea6 100644 --- a/src/vm/opcodes/subscript.rs +++ b/src/vm/opcodes/subscript.rs @@ -1,13 +1,9 @@ -use core::cell::RefCell; -use alloc::{rc::Rc, string::{String, ToString}, vec::Vec}; +use alloc::{string::String, vec::Vec}; use super::super::VM; use super::super::types::*; -use crate::vm::eq::range_len; +pub(crate) use crate::value::host::{normalize_index, slice_bounds}; -fn normalize_index(i: i64, len: usize) -> usize { - (if i < 0 { len as i64 + i } else { i }) as usize -} /* `xs[a:b] = items`, or `del xs[a:b]` without items, on a vector of any item, an extended slice needing an exact count. */ fn splice(b: &mut Vec, (s, e, st): (i64, i64, i64), items: Option>) -> Result<(), VmErr> { @@ -30,25 +26,6 @@ fn splice(b: &mut Vec, (s, e, st): (i64, i64, i64), items: Option>) Ok(()) } -/* Slice bounds clamped to a sequence of `len` items, the triple `slice.indices(len)` returns. */ -pub(crate) fn slice_bounds(start: Val, stop: Val, step: Val, len: i64) -> Result<(i64, i64, i64), VmErr> { - let st = if step.is_none() { 1 } else if step.is_int() { step.as_int() } else { - return Err(cold_type("slice step must be an integer")); - }; - if st == 0 { return Err(cold_value("slice step cannot be zero")); } - let clamp = |v: Val, def: i64| -> i64 { - if v.is_none() { def } - else if v.is_int() { let i = v.as_int(); if i < 0 { (len+i).max(0) } else { i.min(len) } } - else { def } - }; - // Negative step bounds at [-1, len-1], an underflowing index floors at -1, not 0. - let clamp_neg = |v: Val, def: i64| -> i64 { - if v.is_none() { def } - else if v.is_int() { let i = v.as_int(); (if i < 0 { len + i } else { i }).clamp(-1, len - 1) } - else { def } - }; - Ok(if st > 0 { (clamp(start, 0), clamp(stop, len), st) } else { (clamp_neg(start, len - 1), clamp_neg(stop, -1), st) }) -} impl<'a> VM<'a> { @@ -131,19 +108,10 @@ impl<'a> VM<'a> { } } - /* KeyError carrying the text `msg`, as the methods that find nothing to remove raise. */ - pub(crate) fn text_key_error(&mut self, msg: &str) -> VmErr { - match self.heap.alloc(HeapObj::Str(msg.into())) { Ok(k) => self.key_error(k), Err(e) => e } - } /* Instance indexes coerce via `__index__`, including slice bounds. Dict keys never coerce, they look up by hash and eq. */ fn coerce_index(&mut self, cont: Val, idx: Val, chunk: &crate::parser::SSAChunk) -> Result { - // `xs[True]` indexes like `xs[1]`, a dict keeps the bool key. - let keyed = self.heap.dict_cell(cont).is_some(); - if idx.is_bool() && !keyed { return Ok(Val::int(idx.as_bool() as i64)); } - if !idx.is_heap() || keyed { return Ok(idx); } - // An int of a class deriving from int indexes as the int. - if let &HeapObj::Boxed(n) = self.heap.get(idx) && (n.is_int() || n.is_foreign()) { return Ok(n); } + if let Some(i) = self.plain_index(cont, idx) { return Ok(i); } match *self.heap.get(idx) { HeapObj::Instance(..) => match self.try_call_dunder(idx, "__index__", &[], chunk)? { Some(r) if r.is_int() || r.is_bool() => { @@ -166,203 +134,15 @@ impl<'a> VM<'a> { } } - /* No-dunder indexing path. Used by callers without a bytecode frame (FFI re-entry) and as the post-dunder fallback inside `get_item`. */ - pub fn get_item_builtin(&mut self, obj: Val, idx: Val) -> Result<(), VmErr> { - if idx.is_heap() - && let &HeapObj::Slice(start, stop, step) = self.heap.get(idx) { - let v = self.slice_val(obj, start, stop, step)?; - self.push(v); - return Ok(()); - } - - let ascii = idx.is_int() && self.heap.str_is_ascii(obj); - if obj.is_heap() && idx.is_int() - && let HeapObj::Str(s) = self.heap.get(obj) { - let i = idx.as_int(); - let one: String = if ascii { - let b = s.as_bytes(); - let c = *b.get(normalize_index(i, b.len())).ok_or_else(|| cold_index("string index out of range"))?; - (c as char).to_string() - } else { - let ui = normalize_index(i, s.chars().count()); - s.chars().nth(ui).ok_or_else(|| cold_index("string index out of range"))?.to_string() - }; - let val = self.heap.alloc(HeapObj::Str(one))?; - self.push(val); - return Ok(()); - } - - // `bytes[i]` returns the byte as int (`0..=255`), unlike `str[i]` (length-1 str). - if obj.is_heap() && idx.is_int() - && let HeapObj::Bytes(b) | HeapObj::ByteArray(b) = self.heap.get(obj) { - let i = idx.as_int(); - let ui = normalize_index(i, b.len()); - let what = if matches!(self.heap.get(obj), HeapObj::ByteArray(_)) { "bytearray index out of range" } else { "index out of range" }; - let byte = *b.get(ui).ok_or_else(|| cold_index(what))?; - self.push(Val::int(byte as i64)); - return Ok(()); - } - - let v = self.getitem_val(obj, idx)?; - self.push(v); - Ok(()) - } - - fn slice_val(&mut self, obj: Val, start: Val, stop: Val, step: Val) -> Result { - if !obj.is_heap() { return Err(cold_type("slice requires a sequence")); } - if let Some(m) = self.view(obj) { return self.view_slice(m?, start, stop, step); } - - // Item count without materialising the source. - let ascii = self.heap.str_is_ascii(obj); - let len: i64 = match self.heap.get(obj) { - HeapObj::List(v) => v.borrow().len() as i64, - HeapObj::Tuple(v) => v.len() as i64, - HeapObj::Str(s) => if ascii { s.len() as i64 } else { s.chars().count() as i64 }, - HeapObj::Bytes(b) | HeapObj::ByteArray(b) => b.len() as i64, - // A range slices into another range, its bounds resolved at any width. - HeapObj::Range(..) | HeapObj::BigRange(..) => return crate::frontend::int::range_slice(&mut self.heap, obj, start, stop, step).unwrap_or(Err(VmErr::Overflow)), - _ => return Err(cold_type("object is not sliceable")), - }; - // A boxed index lies past either end of any sequence, so it clamps like the inline extreme. - let clip = |v: Val| crate::frontend::int::clip_index(&self.heap, v); - let (s, e, st) = slice_bounds(clip(start), clip(stop), clip(step), len)?; - - // Step 1 copies one contiguous range. - let contiguous = st == 1; - let (lo, hi) = (s.max(0) as usize, e.max(s).max(0) as usize); - let mut indices = Vec::new(); - if !contiguous { - let mut cur = s; - if st > 0 { while cur < e { indices.push(cur as usize); cur += st; } } - else { while cur > e { indices.push(cur as usize); cur += st; } } - } - let pick = |v: &[Val]| -> Vec { - if contiguous { return v[lo.min(v.len())..hi.min(v.len())].to_vec(); } - indices.iter().filter_map(|&i| v.get(i).copied()).collect() - }; - let pick_bytes = |b: &[u8]| -> Vec { - if contiguous { return b[lo.min(b.len())..hi.min(b.len())].to_vec(); } - indices.iter().filter_map(|&i| b.get(i).copied()).collect() - }; - // Pick while borrowing the source, allocate after. - enum Out { List(Vec), Tuple(Vec), Str(String), Bytes(Vec), ByteArray(Vec) } - let out = match self.heap.get(obj) { - HeapObj::List(v) => Out::List(pick(&v.borrow())), - HeapObj::Tuple(v) => Out::Tuple(pick(v)), - HeapObj::Str(text) if ascii => Out::Str(String::from_utf8(pick_bytes(text.as_bytes())).unwrap_or_default()), - HeapObj::Str(text) => { - let chars: Vec = text.chars().collect(); - Out::Str(if contiguous { chars[lo.min(chars.len())..hi.min(chars.len())].iter().collect() } - else { indices.iter().filter_map(|&i| chars.get(i)).collect() }) - } - HeapObj::Bytes(buf) => Out::Bytes(pick_bytes(buf)), - HeapObj::ByteArray(buf) => Out::ByteArray(pick_bytes(buf)), - _ => return Err(cold_type("object is not sliceable")), - }; - match out { - Out::List(v) => self.heap.alloc(HeapObj::List(Rc::new(RefCell::new(v)))), - Out::Tuple(v) => self.heap.alloc(HeapObj::Tuple(v)), - Out::Str(s) => self.heap.alloc(HeapObj::Str(s)), - Out::Bytes(b) => self.heap.alloc(HeapObj::Bytes(b)), - Out::ByteArray(b) => self.heap.alloc(HeapObj::ByteArray(b)), - } - } - pub fn getitem_val(&mut self, obj: Val, idx: Val) -> Result { - if !obj.is_heap() { return Err(VmErr::TypeMsg(crate::s!("'", str self.type_name(obj), "' object is not subscriptable"))); } - let bad = |vm: &Self, kind: &str| if crate::frontend::int::is_big(&vm.heap, idx) { cold_index("cannot fit 'int' into an index-sized integer") } - // A string words it without slices and quotes the type. - else if kind == "string" { VmErr::TypeMsg(crate::s!("string indices must be integers, not '", str vm.type_name(idx), "'")) } - else { VmErr::TypeMsg(crate::s!(str kind, " indices must be integers or slices, not ", str vm.type_name(idx))) }; - if self.heap.dict_cell(obj).is_some() { self.keyed(idx)?; } - match self.heap.get(obj) { - HeapObj::List(v) => { - if !idx.is_int() { return Err(bad(self, "list")); } - let b = v.borrow(); let i = idx.as_int(); - let ui = normalize_index(i, b.len()); - b.get(ui).copied().ok_or_else(|| cold_index("list index out of range")) - } - HeapObj::Tuple(v) => { - if !idx.is_int() { return Err(bad(self, "tuple")); } - let i = idx.as_int(); - let ui = normalize_index(i, v.len()); - v.get(ui).copied().ok_or_else(|| cold_index("tuple index out of range")) - } - HeapObj::Dict(p) | HeapObj::FrozenDict(p) => { - let hit = p.borrow().get(&idx, &self.heap).copied(); - match hit { - Some(v) => Ok(v), - None => { self.require_key(idx)?; Err(self.key_error(idx)) } - } - } - HeapObj::Str(_) => Err(bad(self, "string")), - HeapObj::Bytes(_) => Err(bad(self, "byte")), - HeapObj::ByteArray(_) => Err(bad(self, "bytearray")), - HeapObj::MemoryView(_) => { let m = self.view(obj).unwrap_or(Err(cold_heap()))?; self.view_item(m, idx) } - &HeapObj::BigRange(..) if idx.is_int() || crate::frontend::int::is_big(&self.heap, idx) => crate::frontend::int::range_item(&mut self.heap, obj, idx).unwrap_or(Err(VmErr::Overflow)), - HeapObj::BigRange(..) => Err(bad(self, "range")), - // `range(n)[i]` is computed, never materialised. - &HeapObj::Range(s, e, st) => { - if !idx.is_int() { return Err(bad(self, "range")); } - let (i, len) = (idx.as_int(), range_len(s, e, st) as i64); - let i = if i < 0 { i + len } else { i }; - if !(0..len).contains(&i) { return Err(cold_index("range object index out of range")); } - self.heap.int((s + i * st) as i128) - } - // `list[int]` and `Pair[int]` build a generic alias, the args kept as one tuple. - HeapObj::Type(n) if matches!(n.as_str(), "list" | "tuple" | "dict" | "set" | "frozenset" | "type") => self.generic_alias(obj, idx), - HeapObj::TypeAlias(..) => self.generic_alias(obj, idx), - _ => Err(VmErr::TypeMsg(crate::s!("'", str self.type_name(obj), "' object is not subscriptable"))), - } - } - fn generic_alias(&mut self, origin: Val, idx: Val) -> Result { - let args = if idx.is_heap() && matches!(self.heap.get(idx), HeapObj::Tuple(_)) { idx } else { self.heap.alloc(HeapObj::Tuple(alloc::vec![idx]))? }; - self.heap.alloc(HeapObj::GenericAlias(origin, args)) - } - /* Reject mutable types (list/dict/set) used as dict/set keys, plus instances whose class defines `__eq__`. */ - pub(in crate::vm) fn require_hashable(&self, v: Val) -> Result<(), VmErr> { self.require_hashable_at(v, 0) } - /* A set probe of a set looks up as the frozenset it equals, so only other values must hash. */ - pub(in crate::vm) fn require_set_probe(&self, v: Val) -> Result<(), VmErr> { - if v.is_heap() && matches!(self.heap.get(v), HeapObj::Set(_)) { return Ok(()); } - self.require_element(v) - } - /* `v` as a dict key, an unhashable one named for that use as well as for its type. */ - pub(in crate::vm) fn require_key(&self, v: Val) -> Result<(), VmErr> { self.require_hashable(v).map_err(|e| self.unhashable_as(v, &e, "a dict key")) } - /* `v` as a set element, an unhashable one named for that use as well as for its type. */ - pub(in crate::vm) fn require_element(&self, v: Val) -> Result<(), VmErr> { self.require_hashable(v).map_err(|e| self.unhashable_as(v, &e, "a set element")) } - #[cold] - fn unhashable_as(&self, v: Val, e: &VmErr, what: &str) -> VmErr { - VmErr::TypeMsg(crate::s!("cannot use '", str self.type_name(v), "' as ", str what, " (", str &e.message(), ")")) - } - fn require_hashable_at(&self, v: Val, depth: usize) -> Result<(), VmErr> { - if v.is_heap() && depth <= EQ_DEPTH_MAX { - match self.heap.get(v) { - HeapObj::List(_) => return Err(cold_type("unhashable type: 'list'")), - HeapObj::ByteArray(_) => return Err(cold_type("unhashable type: 'bytearray'")), - HeapObj::MemoryView(m) if m.released => return Err(cold_value("operation forbidden on released memoryview object")), - HeapObj::MemoryView(m) if matches!(self.heap.try_get(m.obj), Some(HeapObj::ByteArray(_))) => return Err(cold_value("cannot hash writable memoryview object")), - HeapObj::Dict(_) => return Err(cold_type("unhashable type: 'dict'")), - HeapObj::Set(_) => return Err(cold_type("unhashable type: 'set'")), - HeapObj::DictView(..) => return Err(VmErr::TypeMsg(crate::s!("unhashable type: '", str self.type_name(v), "'"))), - // A tuple hashes through its items as deep as the hash looks, so each must be hashable too. - HeapObj::Tuple(items) => for &item in items { self.require_hashable_at(item, depth + 1)?; }, - // A frozendict hashes through its values too, its keys hashable already. - HeapObj::FrozenDict(d) => for (_, item) in d.borrow().iter() { self.require_hashable_at(item, depth + 1)?; }, - // A `__eq__` without `__hash__` would let two equal keys hash apart. - HeapObj::Instance(cls, _) if self.hash_hook(*cls) == Some(None) => return Err(VmErr::TypeMsg(crate::s!("unhashable type: '", str self.type_name(v), "'"))), - _ => {} - } - } - Ok(()) - } pub fn store_item(&mut self, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> { let n = self.stack.len(); @@ -488,25 +268,5 @@ impl<'a> VM<'a> { splice(&mut b, bounds, items) } - /* `v` as one byte, the int `bytearray` items must be. */ - pub(crate) fn byte_value(&self, v: Val) -> Result { - let v = crate::value::unboxed(v, &self.heap); - let Some(n) = self.as_i128(v).filter(|_| !v.is_float()) else { - return Err(VmErr::TypeMsg(crate::s!("'", str self.type_name(v), "' object cannot be interpreted as an integer"))); - }; - u8::try_from(n).map_err(|_| cold_value("byte must be in range(0, 256)")) - } - // `slice(stop)` | `slice(start, stop)` | `slice(start, stop, step)`, builtin, usable as a sequence index. - pub fn call_slice(&mut self, argc: u16) -> Result<(), VmErr> { - let args = self.pop_n(argc as usize)?; - let (start, stop, step) = match args.as_slice() { - [stop] => (Val::none(), *stop, Val::none()), - [start, stop] => (*start, *stop, Val::none()), - [start, stop, step] => (*start, *stop, *step), - _ => return Err(cold_type("slice() takes 1 to 3 arguments")), - }; - let v = self.heap.alloc(HeapObj::Slice(start, stop, step))?; - self.push(v); Ok(()) - } } diff --git a/src/vm/registers.rs b/src/vm/registers.rs index a92c6261..c880fed1 100644 --- a/src/vm/registers.rs +++ b/src/vm/registers.rs @@ -7,6 +7,7 @@ use super::cache::OpcodeCache; use super::lower::{self, Code, Facts, Ins}; use super::scope::Kind; use super::types::*; +use crate::vm::natives::Builtins; /* The number a register holds as a float, its int widened. */ #[inline(always)] @@ -928,7 +929,7 @@ impl<'a> VM<'a> { } /* What each slot of a chunk's frame is, and the module it binds into. */ - fn chunk_kinds(&mut self, chunk: &SSAChunk) -> (Rc<[Kind]>, usize) { + pub(crate) fn chunk_kinds(&mut self, chunk: &SSAChunk) -> (Rc<[Kind]>, usize) { let module = self.chunk_module_id(chunk); if let Some(&fi) = self.body_to_fi.get(&(chunk as *const SSAChunk)) { return (self.fn_scope[fi].kinds.clone(), module); } // A class body keeps its namespace in slots, its members come from them. diff --git a/src/vm/scope.rs b/src/vm/scope.rs index 3be961dd..874f627d 100644 --- a/src/vm/scope.rs +++ b/src/vm/scope.rs @@ -113,6 +113,25 @@ pub struct FnScope { pub freevars: Vec<(usize, Option, String)>, } +/* What a name of a module, class or function body is, as the VM resolves it before running. */ +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum Binding { + // A slot of the body's own frame, or a temporary or attribute name no variable takes. + Local, + // A cell a nested function shares, or in a class body one of the function around the class. + Cell, + // A module binding by its index, the builtins under it. + Global(u32), + // A name a class body binds, a member of the class its methods do not see. + Member, + // A plain local of the function defining a class, which the class body reads by value as the class is made. + Lent, +} + +/* Which body a scope belongs to, a function with its index in the VM's table. */ +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum BodyKind { Module, Class, Function(usize) } + fn canon_of(chunk: &SSAChunk, i: usize) -> usize { chunk.canonical.get(i).map_or(i, |&c| c as usize) } @@ -243,6 +262,68 @@ impl<'a> VM<'a> { self.fn_scope.extend(scopes); } + /* The bindings of every module, class and function body under `root`, by body, the module and class ones resolved as `chunk_kinds` and a class's making resolve them. */ + pub(crate) fn body_bindings(&mut self, root: &'a SSAChunk) -> FxHashMap<*const SSAChunk, (BodyKind, Vec)> { + // Each class body under the body that defines it. + fn walk<'c>(c: &'c SSAChunk, modules: &mut Vec<&'c SSAChunk>, definers: &mut Vec<(&'c SSAChunk, &'c SSAChunk)>) { + for k in &c.classes { definers.push((k, c)); walk(k, modules, definers); } + for f in &c.functions { walk(&f.1, modules, definers); } + for e in &c.imports { if let crate::parser::ImportKind::Code(sub) = &e.kind { modules.push(sub); walk(sub, modules, definers); } } + } + let (mut modules, mut definers) = (vec![root], Vec::new()); + walk(root, &mut modules, &mut definers); + let mut out = FxHashMap::default(); + for fi in 0..self.functions.len() { + let kinds = self.fn_scope[fi].kinds.iter().map(|k| match *k { Kind::Local => Binding::Local, Kind::Cell => Binding::Cell, Kind::Global(g) => Binding::Global(g) }).collect(); + out.insert(&self.functions[fi].1 as *const SSAChunk, (BodyKind::Function(fi), kinds)); + } + for m in modules { + let (kinds, _) = self.chunk_kinds(m); + out.insert(m as *const SSAChunk, (BodyKind::Module, kinds.iter().map(|k| match *k { Kind::Global(g) => Binding::Global(g), _ => Binding::Local }).collect())); + } + let definer_of = |c: &SSAChunk| definers.iter().find(|d| core::ptr::eq(d.0, c)).map(|d| d.1); + for &(body, definer) in &definers { + // The cells a class body reads, those of the function around it, through the classes between. + let mut around = definer; + while !self.body_to_fi.contains_key(&(around as *const SSAChunk)) && let Some(d) = definer_of(around) { around = d; } + let fi = self.body_to_fi.get(&(around as *const SSAChunk)).copied(); + let kind_in = |vm: &Self, bare: &str| -> Option { + let fi = fi?; + let all: Vec = around.names.iter().enumerate().filter(|&(_, n)| ssa_strip(n) == bare).filter_map(|(i, _)| vm.fn_scope[fi].kinds.get(i).copied()).collect(); + // The cells around the class come before the function's plain locals. + [Kind::Cell, Kind::Local].into_iter().find(|k| all.contains(k)) + }; + let direct = core::ptr::eq(around, definer); + let module = self.chunk_module_id(body); + let mut var = vec![false; body.names.len()]; + let mut member = vec![false; body.names.len()]; + let mut global = vec![false; body.names.len()]; + for ins in &body.instructions { + let Some(i) = Some(ins.operand as usize).filter(|&i| i < body.names.len()) else { continue }; + match ins.opcode { + OpCode::StoreName | OpCode::Phi => { var[i] = true; member[i] = true; } + OpCode::LoadName | OpCode::Del => var[i] = true, + OpCode::LoadGlobal | OpCode::StoreGlobal | OpCode::Global => { var[i] = true; global[i] = true; } + _ => {} + } + } + let kinds = body.names.iter().enumerate().map(|(i, name)| { + let bare = ssa_strip(name); + if !var[i] || bare.starts_with('#') { return Binding::Local; } + if global[i] { return Binding::Global(self.scopes[module].id(bare)); } + if member[i] { return Binding::Member; } + // A plain local is lent only to a class its function defines itself. + match kind_in(self, bare) { + Some(Kind::Cell) => Binding::Cell, + Some(Kind::Local) if direct => Binding::Lent, + _ => Binding::Global(self.scopes[module].id(bare)), + } + }).collect(); + out.insert(body as *const SSAChunk, (BodyKind::Class, kinds)); + } + out + } + /* The module table a function reads its globals from. */ pub(crate) fn fn_module_id(&self, fi: usize) -> usize { self.fn_module.get(fi).and_then(|m| m.as_ref()).and_then(|spec| self.scope_ids.get(spec).copied()).unwrap_or(0) diff --git a/src/vm/value_ops.rs b/src/vm/value_ops.rs index 60786777..f95b7c38 100644 --- a/src/vm/value_ops.rs +++ b/src/vm/value_ops.rs @@ -1,120 +1,24 @@ use crate::s; use super::types::*; +use super::types::host::coerce_floats; use crate::parser::types::OpCode; use alloc::{string::String, vec::Vec, rc::Rc}; use core::cell::RefCell; -use core::cmp::Ordering; - -/* Cap on nested-container rendering depth, stops self-referential prints from overflowing the stack. */ -pub(crate) const RENDER_DEPTH_MAX: usize = 100; - -/* Cap on total rendered output, bounds breadth the way RENDER_DEPTH_MAX bounds depth. */ -pub(crate) const MAX_REPR_LEN: usize = 1_000_000; - -/* Same cap for `<` descent, self-referential sequences raise RecursionError. */ -const CMP_DEPTH_MAX: usize = 100; - -/* Render `bytes` as `b'...'`, printable ASCII verbatim and the rest escaped, in double quotes when it holds a single quote and no double one. */ -fn format_bytes(buf: &[u8]) -> String { quote_bytes(buf, false) } - -/* `format_bytes`, the single quote escaped even inside double quotes when `escape_single` asks, as a bytearray shows. */ -fn quote_bytes(buf: &[u8], escape_single: bool) -> String { - let quote = if buf.contains(&b'\'') && !buf.contains(&b'"') { b'"' } else { b'\'' }; - let mut out = String::with_capacity(buf.len() + 3); - out.push('b'); - out.push(quote as char); - for &b in buf { - match b { - b'\\' => out.push_str("\\\\"), - b'\'' if quote == b'\'' || escape_single => out.push_str("\\'"), - b'\n' => out.push_str("\\n"), - b'\r' => out.push_str("\\r"), - b'\t' => out.push_str("\\t"), - 0x20..=0x7E => out.push(b as char), - _ => { - out.push_str("\\x"); - const HEX: &[u8; 16] = b"0123456789abcdef"; - out.push(HEX[(b >> 4) as usize] as char); - out.push(HEX[(b & 0x0F) as usize] as char); - } - } - } - out.push(quote as char); - out -} -/* `repr` of a str, quote selection (' unless the text has ' but not ") and backslash escapes for what is not printable, the rest verbatim. */ -fn repr_str(s: &str) -> String { - let mut out = String::with_capacity(s.len() + 2); - push_repr_str(&mut out, s); - out -} -/* `repr_str` of `s` written onto `out`. */ -pub(crate) fn push_repr_str(out: &mut String, s: &str) { - use core::fmt::Write; - let quote = if s.contains('\'') && !s.contains('"') { '"' } else { '\'' }; - out.push(quote); - for c in s.chars() { - match c { - '\\' => out.push_str("\\\\"), - '\n' => out.push_str("\\n"), - '\r' => out.push_str("\\r"), - '\t' => out.push_str("\\t"), - c if c == quote => { out.push('\\'); out.push(c); } - c if !crate::util::uni::is_printable(c) => { - let n = c as u32; - if n <= 0xff { let _ = write!(out, "\\x{:02x}", n); } - else if n <= 0xffff { let _ = write!(out, "\\u{:04x}", n); } - else { let _ = write!(out, "\\U{:08x}", n); } - } - c => out.push(c), - } - } - out.push(quote); -} -/* Coerce a numeric pair to f64, returns None if neither operand is a float. */ -fn coerce_floats(a: Val, b: Val, heap: &HeapPool) -> Option> { - if !a.is_float() && !b.is_float() { return None; } - let (x, y) = (num_f64(a, heap)?, num_f64(b, heap)?); - Some(x.and_then(|x| Ok((x, y?)))) -} + + + + + use super::VM; impl<'a> VM<'a> { - pub fn truthy(&self, v: Val) -> bool { - if v.is_none() || v.is_false() { return false; } - if v.is_true() { return true; } - if v.is_int() { return v.as_int() != 0; } - if v.is_float() { return v.as_float() != 0.0; } - if v.is_foreign() { return self.heap.foreign_truthy(v).unwrap_or(true); } - match self.heap.get(v) { - &HeapObj::Boxed(n) => self.truthy(n), - HeapObj::Str(s) => !s.is_empty(), - HeapObj::Bytes(b) | HeapObj::ByteArray(b) => !b.is_empty(), - HeapObj::MemoryView(m) => m.len > 0, - HeapObj::FnCode(_) => true, - HeapObj::List(l) => !l.borrow().is_empty(), - HeapObj::Tuple(t) => !t.is_empty(), - HeapObj::Dict(d) | HeapObj::FrozenDict(d) => !d.borrow().is_empty(), - HeapObj::Set(s) => !s.borrow().is_empty(), - HeapObj::FrozenSet(s) => !s.is_empty(), - HeapObj::Range(s,e,st) => if *st > 0 { s < e } else { s > e }, - HeapObj::BigRange(..) => crate::frontend::int::range_empty(&self.heap, v) == Some(false), - HeapObj::DictView(d, _) => self.truthy(*d), - HeapObj::Type(_) | HeapObj::Func(..) | HeapObj::Slice(..) | HeapObj::BoundMethod(..) - | HeapObj::NativeFn(_) | HeapObj::Class(..) | HeapObj::BoundUserMethod(..) - | HeapObj::Super(..) | HeapObj::Property(..) | HeapObj::PropertyPart(..) - | HeapObj::StaticMethod(..) | HeapObj::ClassMethod(..) | HeapObj::Instance(..) | HeapObj::Coroutine(..) - | HeapObj::Module(..) | HeapObj::Extern(_) | HeapObj::ExcInstance(..) - | HeapObj::Ellipsis | HeapObj::NotImplemented | HeapObj::GenericAlias(..) | HeapObj::TypeAlias(..) - | HeapObj::Union(_) | HeapObj::TypeVar(_) | HeapObj::Iter(..) | HeapObj::Cell(_) | HeapObj::Code(_) => true, - } - } + #[inline] pub fn truthy(&self, v: Val) -> bool { self.heap.truthy(v) } /* Clone the element set out of a `set` or `frozenset`, None for any other type. */ pub(crate) fn clone_set_items(&self, v: Val) -> Option { @@ -157,12 +61,6 @@ impl<'a> VM<'a> { self.heap.alloc(HeapObj::Set(Rc::new(RefCell::new(s)))) } - /* Alloc a set-algebra result, frozen picks frozenset (left-operand type rule). */ - pub(crate) fn alloc_set_result(&mut self, items: Vec, frozen: bool) -> Result { - let s = ValSet::from_vals(&items, &self.heap); - if frozen { self.heap.alloc(HeapObj::FrozenSet(Rc::new(s))) } - else { self.heap.alloc(HeapObj::Set(Rc::new(RefCell::new(s)))) } - } /* Set binops (|, &, ^, -) over set/frozenset, result frozen iff `a` is frozen. */ // Union/intersection/symmetric-diff/difference items, content membership lets alloc dedup distinct-handle equals. @@ -222,203 +120,29 @@ impl<'a> VM<'a> { Ok(()) } + #[inline(never)] pub fn type_name(&self, v: Val) -> &str { - if v.is_bool() { "bool" } - else if v.is_int() { "int" } - else if v.is_float() { "float" } - else if v.is_none() { "NoneType" } - else if v.is_foreign() { self.heap.foreign_kind(v).map_or("object", |k| k.type_name) } - else { match self.heap.get(v) { - &HeapObj::Boxed(n) => self.type_name(n), - HeapObj::Str(_) => "str", - HeapObj::Bytes(_) => "bytes", - HeapObj::ByteArray(_) => "bytearray", - HeapObj::MemoryView(_) => "memoryview", - HeapObj::FnCode(_) => "code", - HeapObj::List(_) => "list", - HeapObj::Dict(_) => "dict", - HeapObj::FrozenDict(_) => "frozendict", - HeapObj::Set(_) => "set", - HeapObj::FrozenSet(_) => "frozenset", - HeapObj::Tuple(_) => "tuple", - HeapObj::Func(..) => "function", - HeapObj::Type(_) | HeapObj::Class(..) => "type", - HeapObj::Range(..) | HeapObj::BigRange(..) => "range", - HeapObj::Slice(..) => "slice", - // A dunder of a builtin type is a slot wrapper, unbound or bound, a named method a descriptor until bound. - &HeapObj::BoundMethod(recv, id) if self.method_unbound(recv, id) => if is_dunder(id.name()) { "wrapper_descriptor" } else { "method_descriptor" }, - HeapObj::BoundMethod(_, id) if is_dunder(id.name()) => "method-wrapper", - HeapObj::BoundMethod(..) | HeapObj::NativeFn(_) | HeapObj::Extern(_) => "builtin_function_or_method", - HeapObj::BoundUserMethod(..) => "method", - HeapObj::Super(..) => "super", - HeapObj::Property(..) | HeapObj::PropertyPart(..) => "property", - HeapObj::StaticMethod(..) => "staticmethod", - HeapObj::ClassMethod(..) => "classmethod", - HeapObj::Instance(cls, _) => { - if cls.is_heap() && let HeapObj::Class(name, _, _) = self.heap.get(*cls) { - return name; - } - "object" - } - HeapObj::Coroutine(c) => match c.body { - BodyRef::Fn(fi) if !self.is_async_fn(fi) => "generator", - BodyRef::Fn(fi) if self.functions[fi].1.is_generator => "async_generator", - _ => "coroutine", - }, - HeapObj::Module(..) => "module", - HeapObj::ExcInstance(..) => "exception", - HeapObj::Ellipsis => "ellipsis", - HeapObj::NotImplemented => "NotImplementedType", - HeapObj::GenericAlias(..) => "GenericAlias", - HeapObj::TypeAlias(..) => "TypeAliasType", - HeapObj::Union(_) => "Union", - // The stars a type parameter was declared with name its kind. - HeapObj::TypeVar(n) if n.starts_with("**") => "ParamSpec", - HeapObj::TypeVar(n) if n.starts_with('*') => "TypeVarTuple", - HeapObj::TypeVar(_) => "TypeVar", - HeapObj::Iter(_, name) => name, - HeapObj::Cell(_) => "cell", - HeapObj::Code(_) => "code", - HeapObj::DictView(_, View::Keys) => "dict_keys", - HeapObj::DictView(_, View::Values) => "dict_values", - HeapObj::DictView(_, View::Items) => "dict_items", - }} - } - - /* `str` of exception `name` from its args, a KeyError key shows as its repr. */ - fn exc_text(&self, name: &str, args: &[Val], seen: &mut Vec) -> String { - match args { - [] => String::new(), - // A group reads as its message and how many members it holds. - [msg, excs] if super::globals::is_group_type(name) => { - let n = match self.heap.try_get(*excs) { Some(HeapObj::List(l)) => l.borrow().len(), Some(HeapObj::Tuple(t)) => t.len(), _ => 0 }; - let tail = if n == 1 { " sub-exception)" } else { " sub-exceptions)" }; - s!(str &self.display_d(*msg, seen), " (", int n as i64, str tail) - } - [a] if name == "KeyError" => self.repr_d(*a, seen), - [a] => self.display_d(*a, seen), - [no, msg, rest @ ..] if rest.len() <= 1 && super::globals::matches_exc_class(name, "OSError") => { - let mut o = s!("[Errno ", str &self.display_d(*no, seen), "] ", str &self.display_d(*msg, seen)); - if let [f] = rest { o.push_str(": "); o.push_str(&self.repr_d(*f, seen)); } - o - } - _ => { let mut o = s!(cap: 32; "("); self.append_reprs(&mut o, args.iter(), seen); o.push(')'); o } - } - } - - /* The heap slot of an exception's `args` tuple, the key cycle checks use. */ - fn exc_args_slot(&self, inst: Val) -> Option { - let Some(HeapObj::Instance(_, attrs)) = self.heap.try_get(inst) else { return None }; - let args = attrs.borrow().iter().find(|(k, _)| matches!(self.heap.try_get(*k), Some(HeapObj::Str(s)) if s == "args")).map(|(_, v)| v)?; - matches!(self.heap.try_get(args), Some(HeapObj::Tuple(_))).then(|| args.as_heap()) - } - - /* The `args` an exception instance holds, empty for anything else. */ - pub(crate) fn exc_args(&self, inst: Val) -> Vec { - let Some(HeapObj::Instance(_, attrs)) = self.heap.try_get(inst) else { return Vec::new() }; - let args = attrs.borrow().iter().find(|(k, _)| matches!(self.heap.try_get(*k), Some(HeapObj::Str(s)) if s == "args")).map(|(_, v)| v); - match args.and_then(|a| self.heap.try_get(a)) { Some(HeapObj::Tuple(t)) => t.clone(), _ => Vec::new() } - } - - /* Whether function `fi` is an `async def`. */ - #[inline] - pub(crate) fn is_async_fn(&self, fi: usize) -> bool { self.is_async.get(fi).copied().unwrap_or(false) } - - /* Whether `v` is a coroutine or an async generator, which only `async for` and `await` step. */ - pub(crate) fn is_async_object(&self, v: Val) -> bool { - matches!(self.heap.try_get(v), Some(HeapObj::Coroutine(c)) if !matches!(c.body, BodyRef::Fn(fi) if !self.is_async_fn(fi))) - } - - /* TypeError for a binary operator no operand type supports. */ - #[cold] - pub(crate) fn unsupported(&self, op: &str, a: Val, b: Val) -> VmErr { - VmErr::TypeMsg(s!("unsupported operand type(s) for ", str op, ": '", str self.type_name(a), "' and '", str self.type_name(b), "'")) - } - - /* `{a, b}` or `frozenset({a, b})` read in place, `...` for a cycle. */ - fn set_repr<'b>(&self, v: Val, frozen: bool, len: usize, items: impl Iterator, seen: &mut Vec) -> String { - if len == 0 { return (if frozen { "frozenset()" } else { "set()" }).into(); } - let mut out = String::from(if frozen { "frozenset({" } else { "{" }); - let id = v.as_heap(); - if seen.contains(&id) { out.push_str("..."); } else { seen.push(id); self.append_reprs(&mut out, items, seen); seen.pop(); } - out.push_str(if frozen { "})" } else { "}" }); - out - } - - fn append_reprs<'b>(&self, out: &mut String, it: impl Iterator, seen: &mut Vec) { - let mut first = true; - for v in it { - // Bound breadth, a wide structure re-referencing one big child would render without limit. - if out.len() > MAX_REPR_LEN { out.push_str(", ..."); break; } - if !first { out.push_str(", "); } - out.push_str(&self.repr_d(*v, seen)); - first = false; - } - } - - pub fn display(&self, v: Val) -> String { self.display_d(v, &mut Vec::new()) } - - /* The text of a FOREIGN value where no error can surface, its type when the kind refuses. */ - fn foreign_text(&self, v: Val) -> String { - match self.heap.foreign_repr(v) { Some(Ok(s)) => s, _ => s!("<", str self.type_name(v), " object", str &at(v), ">") } - } - - /* Whether builtin method `id` over `recv` is unbound, read off its type, an operator dunder keeping that type as `recv`. */ - #[inline] - pub(crate) fn method_unbound(&self, recv: Val, id: crate::vm::methods::BuiltinMethodId) -> bool { - recv.is_undef() || id.ty() == "op" && matches!(self.heap.try_get(recv), Some(HeapObj::Type(_))) + self.heap.type_name_by(v, &|c| match c.body { + BodyRef::Fn(fi) if !self.is_async_fn(fi) => "generator", + BodyRef::Fn(fi) if self.functions[fi].1.is_generator => "async_generator", + _ => "coroutine", + }) } - /* The type an unbound builtin method belongs to. */ - pub(crate) fn method_owner(&self, recv: Val, id: crate::vm::methods::BuiltinMethodId) -> &str { - match self.heap.try_get(recv) { Some(HeapObj::Type(n)) if !recv.is_undef() => n, _ => id.ty() } - } - /* The qualified name of function `fi`, `` for an anonymous one. */ - fn fn_qualname(&self, fi: usize) -> &str { - match self.function_qualnames.get(fi) { Some(q) if !q.is_empty() => q, _ => match self.function_names.get(fi) { Some(n) if !n.is_empty() => n, _ => "" } } - } + // Builtin str and repr with no dunder run. + pub fn display(&self, v: Val) -> String { Host::display(self, v) } + pub fn repr(&self, v: Val) -> String { Host::repr(self, v) } - /* Cycle-aware display, `seen` tracks containers on the current path, so self-referential structures emit "..." instead of recursing forever (and its length bounds raw nesting depth). */ - fn display_d(&self, v: Val, seen: &mut Vec) -> String { - if seen.len() > RENDER_DEPTH_MAX { return "...".into(); } - if v.is_int() { let mut b = itoa::Buffer::new(); return b.format(v.as_int()).into(); } - if v.is_float() { - // Single source of truth for float text (Python repr rules, scientific + .0). - return crate::util::fstr::format_f64(v.as_float()); - } - if v.is_true() { return "True".into(); } - if v.is_false() { return "False".into(); } - if v.is_none() { return "None".into(); } - if v.is_foreign() { return self.foreign_text(v); } + // Text of what only the VM makes. + pub(crate) fn display_vm(&self, v: Val, seen: &mut Vec) -> String { match self.heap.get(v) { - &HeapObj::Boxed(n) => self.display_d(n, seen), - HeapObj::Str(s) => s.clone(), - HeapObj::Bytes(b) => format_bytes(b), - HeapObj::ByteArray(b) => s!("bytearray(", str "e_bytes(b, true), ")"), - HeapObj::MemoryView(m) => s!(str if m.released { ""), &HeapObj::FnCode(fi) => { let name = self.function_names.get(fi).map_or("", |n| if n.is_empty() { "" } else { n }); let body = self.functions.get(fi).map(|f| &f.1); s!("") } - HeapObj::Type(name) => s!(""), &HeapObj::Func(i, ..) => s!(""), - HeapObj::Slice(s,e,st) => s!("slice(", str &self.display_d(*s, seen), ", ", str &self.display_d(*e, seen), ", ", str &self.display_d(*st, seen), ")"), - HeapObj::Range(s,e,st) => if *st == 1 { s!("range(", int *s, ", ", int *e, ")") } else { s!("range(", int *s, ", ", int *e, ", ", int *st, ")") }, - HeapObj::BigRange(..) => crate::frontend::int::range_repr(&self.heap, v).and_then(Result::ok).unwrap_or_else(|| "range(...)".into()), - HeapObj::List(l) => { let id = v.as_heap(); if seen.contains(&id) { return "[...]".into(); } seen.push(id); let mut o = s!(cap: 32; "["); self.append_reprs(&mut o, l.borrow().iter(), seen); o.push(']'); seen.pop(); o }, - HeapObj::Tuple(t) => { let id = v.as_heap(); if seen.contains(&id) { return "(...)".into(); } seen.push(id); let o = if t.len() == 1 { s!("(", str &self.repr_d(t[0], seen), ",)") } else { let mut o = s!(cap: 32; "("); self.append_reprs(&mut o, t.iter(), seen); o.push(')'); o }; seen.pop(); o }, - HeapObj::Dict(d) => self.dict_repr(v, &d.borrow(), seen), - HeapObj::FrozenDict(d) if d.borrow().is_empty() => "frozendict()".into(), - HeapObj::FrozenDict(d) => s!("frozendict(", str &self.dict_repr(v, &d.borrow(), seen), ")"), - // `object` spells its slot wrappers without the type. - &HeapObj::BoundMethod(recv, id) if self.method_unbound(recv, id) && is_dunder(id.name()) && id.ty() == "object" => s!(""), - &HeapObj::BoundMethod(recv, id) if self.method_unbound(recv, id) && is_dunder(id.name()) => s!(""), - &HeapObj::BoundMethod(recv, id) if self.method_unbound(recv, id) => s!(""), - &HeapObj::BoundMethod(recv, id) if is_dunder(id.name()) => s!(""), - &HeapObj::BoundMethod(recv, id) => s!(""), - HeapObj::NativeFn(id) => s!(""), // User classes live in `__main__`, Python qualifies the repr with the module. HeapObj::Class(name, _, _) => crate::s!(""), HeapObj::Instance(cls, _) => { @@ -443,39 +167,10 @@ impl<'a> VM<'a> { let name = match self.heap.try_get(func) { Some(&HeapObj::Func(i, ..)) => self.fn_qualname(i), _ => "?" }; s!("") } - &HeapObj::Super(cls, recv) => { - let short = |c: Val| match self.heap.try_get(c) { Some(HeapObj::Class(n, ..) | HeapObj::Type(n)) => n.as_str(), _ => "?" }; - // A class bound reads as an object of that class, as an instance reads by its type. - if recv.is_none() { return s!(", NULL>"); } - let of = if matches!(self.heap.try_get(recv), Some(HeapObj::Class(..))) { short(recv) } else { self.type_name(recv) }; - s!(", <", str of, " object>>") - } - HeapObj::Property(..) => s!(""), - HeapObj::PropertyPart(_, slot) => crate::s!(""), - &HeapObj::StaticMethod(f) => s!(""), - &HeapObj::ClassMethod(f) => s!(""), HeapObj::Coroutine(c) => { let name = match c.body { BodyRef::Fn(fi) => self.fn_qualname(fi), BodyRef::Module => "" }; s!("<", str self.type_name(v), " object ", str name, str &at(v), ">") } - HeapObj::Iter(_, name) => s!("<", str name, " object", str &at(v), ">"), - HeapObj::DictView(d, kind) => { - let mut o = s!(str self.type_name(v), "(["); - if let Some(m) = self.heap.dict_cell(*d) { - for (i, (k, val)) in m.borrow().iter().enumerate() { - if i > 0 { if o.len() > MAX_REPR_LEN { o.push_str(", ..."); break; } o.push_str(", "); } - match kind { - View::Keys => o.push_str(&self.repr_d(k, seen)), - View::Values => o.push_str(&self.repr_d(val, seen)), - View::Items => { o.push('('); o.push_str(&self.repr_d(k, seen)); o.push_str(", "); o.push_str(&self.repr_d(val, seen)); o.push(')'); } - } - } - } - o.push_str("])"); - o - } - HeapObj::Module(name, _) => s!(""), - HeapObj::Extern(f) => s!(""), HeapObj::GenericAlias(origin, args) => { let mut o = self.alias_arg(*origin, seen); o.push('['); @@ -489,8 +184,6 @@ impl<'a> VM<'a> { o.push(']'); o } - HeapObj::TypeAlias(name, _) => name.clone(), - HeapObj::TypeVar(name) => name.trim_start_matches('*').into(), HeapObj::Union(args) => { let mut o = String::new(); if let HeapObj::Tuple(t) = self.heap.get(*args) { @@ -503,39 +196,61 @@ impl<'a> VM<'a> { } o } - HeapObj::ExcInstance(name, args, _) => self.exc_text(name, args, seen), - HeapObj::Set(s) => { let s = s.borrow(); self.set_repr(v, false, s.len(), s.iter(), seen) } - HeapObj::FrozenSet(s) => self.set_repr(v, true, s.len(), s.iter(), seen), - HeapObj::Ellipsis => "Ellipsis".into(), - HeapObj::NotImplemented => "NotImplemented".into(), - // A cell names what it holds by type, an unfilled one is empty. - &HeapObj::Cell(c) if c.is_undef() => s!(""), - &HeapObj::Cell(c) => s!(""), - HeapObj::Code(c) => s!("", str &at(v), ", file \"", str &c.filename, "\", line 1>"), + _ => s!("<", str self.type_name(v), " object", str &at(v), ">"), } } - /* `{k: v, ...}` of the entries of dict `v`, `{...}` where it holds itself. */ - fn dict_repr(&self, v: Val, d: &DictMap, seen: &mut Vec) -> String { - let id = v.as_heap(); - if seen.contains(&id) { return "{...}".into(); } - seen.push(id); - let mut o = s!(cap: 32; "{"); - for (i, (k, val)) in d.iter().enumerate() { - if i > 0 { - if o.len() > MAX_REPR_LEN { o.push_str(", ..."); break; } - o.push_str(", "); - } - o.push_str(&self.repr_d(k, seen)); - o.push_str(": "); - o.push_str(&self.repr_d(val, seen)); - } - o.push('}'); - seen.pop(); - o + /* An exception instance reads as its constructor call, `E('x')`. */ + pub(crate) fn repr_vm(&self, v: Val, seen: &mut Vec) -> Option { + let &HeapObj::Instance(cls, _) = self.heap.get(v) else { return None }; + self.exc_base(cls)?; + let slot = self.exc_args_slot(v); + if slot.is_some_and(|s| seen.contains(&s)) { return Some(s!(str self.type_name(v), "(...)")); } + seen.extend(slot); + let mut o = s!(cap: 32; str self.type_name(v), "("); + self.append_reprs(&mut o, self.exc_args(v).iter(), seen); + o.push(')'); + if slot.is_some() { seen.pop(); } + Some(o) } - pub fn repr(&self, v: Val) -> String { self.repr_d(v, &mut Vec::new()) } + /* The heap slot of an exception's `args` tuple, the key cycle checks use. */ + fn exc_args_slot(&self, inst: Val) -> Option { + let Some(HeapObj::Instance(_, attrs)) = self.heap.try_get(inst) else { return None }; + let args = attrs.borrow().iter().find(|(k, _)| matches!(self.heap.try_get(*k), Some(HeapObj::Str(s)) if s == "args")).map(|(_, v)| v)?; + matches!(self.heap.try_get(args), Some(HeapObj::Tuple(_))).then(|| args.as_heap()) + } + + /* The `args` an exception instance holds, empty for anything else. */ + pub(crate) fn exc_args(&self, inst: Val) -> Vec { + let Some(HeapObj::Instance(_, attrs)) = self.heap.try_get(inst) else { return Vec::new() }; + let args = attrs.borrow().iter().find(|(k, _)| matches!(self.heap.try_get(*k), Some(HeapObj::Str(s)) if s == "args")).map(|(_, v)| v); + match args.and_then(|a| self.heap.try_get(a)) { Some(HeapObj::Tuple(t)) => t.clone(), _ => Vec::new() } + } + + /* Whether function `fi` is an `async def`. */ + #[inline] + pub(crate) fn is_async_fn(&self, fi: usize) -> bool { self.is_async.get(fi).copied().unwrap_or(false) } + + /* Whether `v` is a coroutine or an async generator, which only `async for` and `await` step. */ + pub(crate) fn is_async_object(&self, v: Val) -> bool { + matches!(self.heap.try_get(v), Some(HeapObj::Coroutine(c)) if !matches!(c.body, BodyRef::Fn(fi) if !self.is_async_fn(fi))) + } + + + + + + + + + /* The qualified name of function `fi`, `` for an anonymous one. */ + fn fn_qualname(&self, fi: usize) -> &str { + match self.function_qualnames.get(fi) { Some(q) if !q.is_empty() => q, _ => match self.function_names.get(fi) { Some(n) if !n.is_empty() => n, _ => "" } } + } + + + /* An alias argument as Python spells it, a type by its name and anything else by repr. */ fn alias_arg(&self, v: Val, seen: &mut Vec) -> String { @@ -552,184 +267,11 @@ impl<'a> VM<'a> { self.repr_d(v, seen) } - pub(crate) fn repr_d(&self, v: Val, seen: &mut Vec) -> String { - if v.is_heap() { - match self.heap.get(v) { - HeapObj::Str(s) => return repr_str(s), - // `repr(E("x"))` is the constructor call, `ValueError('x')`. - HeapObj::ExcInstance(name, args, _) => { - let mut o = s!(cap: 32; str name, "("); - self.append_reprs(&mut o, args.iter(), seen); - o.push(')'); - return o; - } - // An exception instance reads as its constructor call, `E('x')`. - &HeapObj::Instance(cls, _) if self.exc_base(cls).is_some() => { - let slot = self.exc_args_slot(v); - if slot.is_some_and(|s| seen.contains(&s)) { return s!(str self.type_name(v), "(...)"); } - seen.extend(slot); - let mut o = s!(cap: 32; str self.type_name(v), "("); - self.append_reprs(&mut o, self.exc_args(v).iter(), seen); - o.push(')'); - if slot.is_some() { seen.pop(); } - return o; - } - _ => {} - } - } - self.display_d(v, seen) - } - pub fn lt_vals(&self, a: Val, b: Val) -> Result { - self.lt_vals_d(a, b, 0) - } - /* Depth-tracked `<`, past the cap surface RecursionError instead of overflowing the stack. */ - fn lt_vals_d(&self, a: Val, b: Val, depth: usize) -> Result { - if depth > CMP_DEPTH_MAX { return Err(cold_depth()); } - let a = if a.is_bool() { Val::int(a.as_bool() as i64) } else { a }; - let b = if b.is_bool() { Val::int(b.as_bool() as i64) } else { b }; - if a.is_int() && b.is_int() { return Ok(a.as_int() < b.as_int()); } - if (a.is_foreign() || b.is_foreign()) && let Some(o) = self.heap.foreign_cmp(a, b) { return Ok(o == Ordering::Less); } - if let Some(Ok((af, bf))) = coerce_floats(a, b, &self.heap) { return Ok(af < bf); } - if a.is_heap() && b.is_heap() { - match (self.heap.get(a), self.heap.get(b)) { - (HeapObj::Str(x), HeapObj::Str(y)) => return Ok(x < y), - (HeapObj::Bytes(x) | HeapObj::ByteArray(x), HeapObj::Bytes(y) | HeapObj::ByteArray(y)) => return Ok(x < y), - // Sequences compare lexicographically, clone to drop the heap borrow before recursing. - (HeapObj::List(x), HeapObj::List(y)) => { - let (x, y) = (x.borrow().clone(), y.borrow().clone()); - return self.seq_lt_d(&x, &y, depth + 1); - } - (HeapObj::Tuple(x), HeapObj::Tuple(y)) => { - let (x, y) = (x.clone(), y.clone()); - return self.seq_lt_d(&x, &y, depth + 1); - } - _ => {} - } - } - // A number of a class over int or float orders as the number. - let (x, y) = (unboxed(a, &self.heap), unboxed(b, &self.heap)); - if x.0 != a.0 || y.0 != b.0 { return self.lt_vals_d(x, y, depth); } - Err(VmErr::TypeMsg(s!("'<' not supported between instances of '", str self.type_name(a), "' and '", str self.type_name(b), "'"))) - } - /* Lexicographic `<` for sequences, first differing element decides, otherwise the shorter is less. Recurses through `lt_vals`, so nested sequences and mixed element types are handled (and rejected) consistently. */ - fn seq_lt_d(&self, xs: &[Val], ys: &[Val], depth: usize) -> Result { - if depth > CMP_DEPTH_MAX { return Err(cold_depth()); } - for (&x, &y) in xs.iter().zip(ys.iter()) { - if eq_member(x, y, &self.heap) { continue; } - return self.lt_vals_d(x, y, depth + 1); - } - Ok(xs.len() < ys.len()) - } - /* Item presence in list/tuple/dict/set, or substring in string. Non-iterable container raises TypeError. */ - pub fn contains(&mut self, container: Val, item: Val) -> Result { - if container.is_heap() && matches!(self.heap.get(container), HeapObj::Dict(_) | HeapObj::FrozenDict(_) | HeapObj::Set(_) | HeapObj::FrozenSet(_)) { self.keyed(item)?; } - if container.is_heap() { - match self.heap.get(container) { - // A scan counts what it may read as native work. - HeapObj::List(v) => { self.heap.spend(v.borrow().len()); return Ok(v.borrow().iter().any(|x| eq_member(*x, item, &self.heap))) } - HeapObj::Tuple(v) => { self.heap.spend(v.len()); return Ok(v.iter().any(|x| eq_member(*x, item, &self.heap))) } - // Only a miss checks the item, an unhashable one is never inside so a hit needs no check. - HeapObj::Dict(p) | HeapObj::FrozenDict(p) => { let hit = p.borrow().contains_key(&item, &self.heap); if !hit { self.require_key(item)?; } return Ok(hit); } - HeapObj::Set(s) => { let hit = s.borrow().contains(item, &self.heap); if !hit { self.require_set_probe(item)?; } return Ok(hit); } - HeapObj::FrozenSet(s) => { let hit = s.contains(item, &self.heap); if !hit { self.require_set_probe(item)?; } return Ok(hit); } - HeapObj::Str(s) => { - if let Some(HeapObj::Str(sub)) = self.heap.try_get(item) { self.heap.spend(s.len()); return Ok(s.contains(sub.as_str())); } - return Err(VmErr::TypeMsg(s!("'in ' requires string as left operand, not ", str self.type_name(item)))); - } - HeapObj::Bytes(b) | HeapObj::ByteArray(b) => { - if let Some(sub) = self.heap.bytes_of(item) { - self.heap.spend(b.len()); - return Ok(sub.is_empty() || b.windows(sub.len()).any(|w| w == sub)); - } - let n = match item { i if i.is_int() => i.as_int(), i if i.is_bool() => i.as_bool() as i64, _ => return Err(cold_type("a bytes-like object is required")) }; - if !(0..=255).contains(&n) { return Err(cold_value("byte must be in range(0, 256)")); } - return Ok(b.contains(&(n as u8))); - } - HeapObj::BigRange(..) => return Ok(crate::frontend::int::range_has(&self.heap, container, item).unwrap_or(false)), - HeapObj::Range(s, e, st) => { - let (s, e, st) = (*s as i128, *e as i128, *st as i128); - // O(1) range membership via bounds + step, integral floats match too. - let x = match as_i128(item, &self.heap) { - Some(i) => Some(i), - None if item.is_float() => { - let v = item.as_float(); - if v.is_finite() && v == libm::trunc(v) { Some(v as i128) } else { None } - } - None => None, - }; - return Ok(match x { - Some(x) => { - let in_bounds = if st > 0 { x >= s && x < e } else { x <= s && x > e }; - in_bounds && st != 0 && (x - s) % st == 0 - } - None => false, - }); - } - _ => {} - } - } - Err(VmErr::TypeMsg(s!("argument of type '", str self.type_name(container), "' is not a container or iterable"))) - } - pub fn add_vals(&mut self, a: Val, b: Val) -> Result { - // Inline-int fast path, overflow falls through to the i128 slow path. - if a.is_int() && b.is_int() - && let Some(r) = a.as_int().checked_add(b.as_int()) - && (Val::INT_MIN..=Val::INT_MAX).contains(&r) { - return Ok(Val::int(r)); - } - if let Some(p) = coerce_floats(a, b, &self.heap) { let (af, bf) = p?; return Ok(Val::float(af + bf)); } - if let (Some(ai), Some(bi)) = (as_i128(a, &self.heap), as_i128(b, &self.heap)) && let Some(r) = ai.checked_add(bi) { return self.heap.int(r); } - if let Some(r) = self.wide_op(BinOp::Add, a, b)? { return Ok(r); } - if a.is_heap() && b.is_heap() { - // Charge the copy cost so growing concatenation in a loop stays bounded (avoids O(n^2)). - let copy_cost = match (self.heap.get(a), self.heap.get(b)) { - (HeapObj::Str(sa), HeapObj::Str(sb)) => Some(sa.len() + sb.len()), - (HeapObj::Bytes(ba) | HeapObj::ByteArray(ba), HeapObj::Bytes(bb) | HeapObj::ByteArray(bb)) => Some(ba.len() + bb.len()), - (HeapObj::List(va), HeapObj::List(vb)) => Some(va.borrow().len() + vb.borrow().len()), - (HeapObj::Tuple(va), HeapObj::Tuple(vb)) => Some(va.len() + vb.len()), - _ => None, - }; - if let Some(n) = copy_cost { self.charge_steps(n)?; } - match (self.heap.get(a), self.heap.get(b)) { - (HeapObj::Str(sa), HeapObj::Str(sb)) => { - let mut r = String::with_capacity(sa.len() + sb.len()); - r.push_str(sa); r.push_str(sb); - return self.heap.alloc(HeapObj::Str(r)); - } - // Each result is sized once, so no operand is copied twice. - (HeapObj::List(va), HeapObj::List(vb)) => { - let lst = [&va.borrow()[..], &vb.borrow()[..]].concat(); - return self.heap.alloc(HeapObj::List(Rc::new(RefCell::new(lst)))); - } - (HeapObj::Tuple(va), HeapObj::Tuple(vb)) => { - let tup = [&va[..], &vb[..]].concat(); - return self.heap.alloc(HeapObj::Tuple(tup)); - } - // The result takes the type of the left side. - (left @ (HeapObj::Bytes(ba) | HeapObj::ByteArray(ba)), HeapObj::Bytes(bb) | HeapObj::ByteArray(bb)) => { - let r = [&ba[..], &bb[..]].concat(); - return self.heap.alloc(if matches!(left, HeapObj::ByteArray(_)) { HeapObj::ByteArray(r) } else { HeapObj::Bytes(r) }); - } - _ => {} - } - } - Err(self.concat_err(a, b).unwrap_or_else(|| self.unsupported("+", a, b))) - } - /* The error a sequence on the left of `+` raises for a right side of another type. */ - #[cold] - fn concat_err(&self, a: Val, b: Val) -> Option { - let (left, right) = (self.type_name(a), self.type_name(b)); - match self.heap.try_get(a)? { - HeapObj::Str(_) | HeapObj::List(_) | HeapObj::Tuple(_) => Some(VmErr::TypeMsg(s!("can only concatenate ", str left, " (not \"", str right, "\") to ", str left))), - HeapObj::Bytes(_) | HeapObj::ByteArray(_) => Some(VmErr::TypeMsg(s!("can't concat ", str right, " to ", str left))), - _ => None, - } - } pub fn sub_vals(&mut self, a: Val, b: Val) -> Result { if a.is_int() && b.is_int() @@ -832,11 +374,6 @@ impl<'a> VM<'a> { Ok(Val::float(av / bv)) } - /* `op` through the kind of a FOREIGN operand, None when nothing takes it. */ - pub(crate) fn wide_op(&mut self, op: BinOp, a: Val, b: Val) -> Result, VmErr> { - if !a.is_foreign() && !b.is_foreign() { return Ok(None); } - self.heap.foreign_binop(op, a, b) - } /* Method wrapper around the free `as_i128` for borrow-checker ergonomics. */ #[inline] @@ -844,14 +381,7 @@ impl<'a> VM<'a> { as_i128(v, &self.heap) } - /* Wrap an i128 into the narrowest Val, None->Overflow, 48-bit->inline, else boxed. */ - #[inline] - pub(crate) fn int_to_val(&mut self, r: Option) -> Result { - self.heap.int(r.ok_or_else(cold_overflow)?) - } } -/* The ` at 0x...` a repr places an object by, the word `id` gives. */ -fn at(v: Val) -> String { alloc::format!(" at 0x{:x}", v.0) } -fn is_dunder(name: &str) -> bool { name.len() > 4 && name.starts_with("__") && name.ends_with("__") } +pub(crate) use super::types::render::{MAX_REPR_LEN, RENDER_DEPTH_MAX, at, push_repr_str}; diff --git a/src/wasm/exports.rs b/src/wasm/exports.rs index 9d371ec4..b3781ba0 100644 --- a/src/wasm/exports.rs +++ b/src/wasm/exports.rs @@ -49,7 +49,7 @@ fn parse_source(src: &str) -> Result, String> { p.chunk.warnings.retain(|w| w.start < first); return Err(s!(str &Diagnostic::render_warnings(&p.chunk.warnings, src, name.as_deref()), str &report)); } - crate::vm::optimizer::constant_fold(&mut p.chunk); + crate::optimizer::constant_fold(&mut p.chunk); Ok(Rc::new(p.chunk)) } diff --git a/tests/cases/vm.json b/tests/cases/vm.json index 5767ee2d..2ed846e3 100644 --- a/tests/cases/vm.json +++ b/tests/cases/vm.json @@ -7492,6 +7492,7 @@ {"src": "# functions taking **kwargs past parameters only a keyword passes, defaults and *args, called by position and by keyword\ndef kw(n, *, z=0, **rest):\n rest['seen'] = len(rest)\n return n + z + rest.get('x', 0) + rest.get('y', 0) + rest['seen']\ndef d(n, z=5, *more, w=2, **rest):\n return n + z + len(more) * 10 + w * 100 + len(rest) * 1000\ndef f(i):\n return kw(i, z=1) + kw(i, y=2, x=3) + g(i)\ndef g(i):\n return d(i) + d(i, 1, 2, 3) + d(i, w=0, q=1)\ndef rec(n, *, acc=0, **opts):\n return acc + opts.get('bonus', 0) if n == 0 else rec(n - 1, acc=acc + n, bonus=1)\nt = 0\nfor i in range(300):\n t += d(n=i, z=0) + f(i) + kw(i)\nprint(t)\ns = 0\nfor i in range(100):\n s += rec(30 + i % 4)\nprint(s)", "output": ["805650", "51350"]}, {"src": "# a module binding a section calls rebound mid-loop from a bound method to a function, a lambda and back, and from one instance with a __call__ to another class's\nclass A:\n def __init__(self, k):\n self.k = k\n def m(self, n):\n return n + self.k\nclass C:\n def __call__(self, n):\n return n * 2\nclass D:\n def __call__(self, n):\n return -n\nbm = A(1).m\ninst = C()\ndef other(n):\n return 7\ndef f(i):\n return bm(i) + inst(i)\nt = 0\nfor i in range(4000):\n if i == 1000:\n inst = D()\n if i == 1500:\n bm = other\n if i == 2500:\n bm = lambda n: n % 5\n if i == 3500:\n bm = A(3).m\n t += f(i)\nprint(t)", "output": ["-3488500"]}, {"src": "# functions taking **kwargs or *args defined after the code that calls them by keyword was laid\ndef f(i):\n return i\nt = 0\nfor i in range(300):\n t += f(i)\ndef kw(n, *, z=0, **rest):\n return n + z + len(rest) + rest.get('y', 0)\ndef st(*a, **k):\n return len(a) + len(k)\ndef dd(a, b=2, *more, c=3, **k):\n return a + b * 10 + len(more) * 100 + c * 1000 + len(k) * 10000\ns = 0\nfor i in range(3000):\n s += kw(i, z=1) + kw(i, y=2) + st(i, q=i) + dd(i, c=1) + dd(i, 5, 6, 7, x=1)\nprint(t, s)", "output": ["44850 60822000"]}, + {"src": "print('Straße'.casefold(), 'fi'.casefold(), 'ΣΑΣ'.casefold(), 'ᾈ'.casefold(), 'Ꭰ'.casefold() == 'Ꭰ'.casefold(), 'µ'.casefold() == 'μ')", "output": ["strasse fi σασ ἀι True True"]}, {"src": "class K:\n def __init__(s, v): s.v = v\n def __hash__(s): return s.v % 3\n def __eq__(s, o): return isinstance(o, K) and s.v == o.v\n def __repr__(s): return 'K%d' % s.v\ns, d = set(), {}\nfor i in range(120):\n s.add(K(i % 9))\n d[K(i % 5)] = d.get(K(i % 5), 0) + 1\n d.setdefault(K(i % 4), i)\nprint(len(s), sorted(k.v for k in d), d[K(2)], K(3) in s, (K(1), 2) in {(K(1), 2)})\nprint(sorted({K(i % 4) for i in range(30)}, key=lambda k: k.v), len({K(1): 1, K(1): 2, K(4): 3}), {K(1), K(2), K(1)} == {K(2), K(1)})\nclass Bad:\n def __hash__(s): raise ValueError('no hash')\ntry:\n s.add(Bad())\nexcept ValueError as e:\n print('ValueError', e, len(s))\nclass Eq:\n def __hash__(s): return 1000003\n def __eq__(s, o): raise KeyError('eq')\nt = {Eq()}\ntry:\n t.add(Eq())\nexcept KeyError as e:\n print('KeyError', e, len(t))\nclass P:\n made = 0\n def __new__(cls, x):\n o = super().__new__(cls)\n P.made += 1\n return o\n def __init__(self, x):\n self.x = x * 2\nclass Q(P):\n def __init__(self, x):\n super().__init__(x)\n self.y = 1\ntot = 0\nfor i in range(150):\n tot += P(i).x + Q(i).y\nprint(tot, P.made, type(Q(1)).__name__)\nclass N:\n def __new__(cls, x):\n if x < 0: raise ValueError('neg')\n return super().__new__(cls)\ntry:\n N(-1)\nexcept ValueError as e:\n print('ValueError', e)\nclass Other:\n def __new__(cls): return 42\n def __init__(self): print('never')\nprint(Other())\nclass Counter:\n def __init__(self): self.n = 0\n def __get__(self, o, t):\n self.n += 1\n return self.n\n def __set__(self, o, v):\n o.__dict__['seen'] = v\nclass A:\n c = Counter()\n def __init__(self): self.log = 0\na = A()\nt = 0\nfor i in range(200):\n t += a.c\n a.c = i\nprint(t, a.seen)\nclass W:\n def __setattr__(s, n, v):\n object.__setattr__(s, n, v * 2 if isinstance(v, int) else v)\nw = W()\nfor i in range(100):\n w.x = i\nprint(w.x)", "output": ["9 [0, 1, 2, 3, 4] 24 True True", "[K0, K1, K2, K3] 2 True", "ValueError no hash 9", "KeyError 'eq' 1", "22500 300 Q", "ValueError neg", "42", "20100 199", "198"]}, {"src": "class Other:\n def __new__(cls): return 42\n def __init__(self): print('never')\nprint(Other())\nclass P:\n made = 0\n def __new__(cls, x):\n o = super().__new__(cls)\n P.made += 1\n return o\n def __init__(self, x):\n self.x = x\nfor i in range(150):\n P(i)\nprint(P.made)\nfor i in range(100):\n o = Other()\nprint(o)", "output": ["42", "150", "42"]}, {"src": "class G:\n def __get__(self, o, t):\n raise AttributeError('gone')\nclass H:\n g = G()\n def __getattr__(self, n): return 'fallback ' + n\nprint(H().g)\nclass Pr:\n @property\n def p(self): raise AttributeError('no p')\n def __getattr__(self, n): return 'via ' + n\nprint(Pr().p)\nclass N:\n g = G()\ntry:\n N().g\nexcept AttributeError as e:\n print('AttributeError', e)\nclass Eq:\n calls = 0\n def __hash__(s): return 1\n def __eq__(s, o):\n Eq.calls += 1\n return NotImplemented\nclass K:\n def __init__(s, v): s.v = v\n def __hash__(s): return s.v\n def __eq__(s, o): return isinstance(o, K) and s.v == o.v\nks = {K(1), K(2)}\nt = {Eq()}\nprint(len(t), Eq.calls, K(1) in ks)\nu = {Eq(), Eq()}\nprint(len(u), Eq.calls > 0)\nclass Sub(K): pass\nprint(Sub(1) in ks, K(2) in {Sub(2)})", "output": ["fallback g", "via p", "AttributeError gone", "1 0 True", "2 True", "True True"]}, @@ -7512,6 +7513,9 @@ {"src": "def g(n):\n for i in range(n): yield i\nclass K:\n def __init__(s, v): s.v = v\n def __eq__(s, o): return s.v == o\n def __hash__(s): return hash(s.v)\nr = []\nr.append(list(zip(g(3), g(3))))\nr.append(dict(zip(g(3), g(3))))\nr.append(set(map(lambda x: x, g(4))))\nr.append(sorted(g(4), key=lambda x: -x))\nr.append(tuple(enumerate(g(3))))\nr.append(3 in g(5))\nr.append(K(2) in g(5))\nr.append(sum(x for x in g(5)))\nr.append(max(g(5)), )\nr.append(min(g(5), default=0))\nr.append(any(g(3)))\nr.append(all(g(3)))\nr.append(list(filter(None, g(4))))\nr.append(''.join(str(x) for x in g(3)))\nr.append([*g(3)])\na, b, c = g(3); r.append((a, b, c))\nr.append(list(reversed(list(g(3)))))\nr.append(next(iter(g(3))))\nit = g(3); r.append([next(it), next(it), next(it, 'd'), next(it, 'e')])\nr.append(list(map(next, [g(2), g(3)])))\nr.append(sorted(map(next, [g(2), g(3)])))\nr.append(dict.fromkeys(g(3)))\nx = [0]; x.extend(g(3)); r.append(x)\nx = [0]; x += g(3); r.append(x)\ns = set(); s.update(g(3)); r.append(s)\nr.append(bytes(g(3)))\nr.append(bytearray(g(3)))\nr.append(list(map(lambda a, b: a + b, g(3), g(3))))\nr.append(frozenset(g(3)))\nr.append(str(list(g(2))))\nr.append(divmod(*g(2)) if False else 0)\nr.append(list(zip(*[g(2), g(2)])))\nr.append(min(g(3), key=lambda v: -v))\nr.append(len(list(g(4))))\ngs = g(4); next(gs); r.append(list(gs))\ndef h():\n yield from g(3)\nr.append(list(h()))\nr.append(isinstance(g(1), object))\nprint(r)", "output": ["[[(0, 0), (1, 1), (2, 2)], {0: 0, 1: 1, 2: 2}, {0, 1, 2, 3}, [3, 2, 1, 0], ((0, 0), (1, 1), (2, 2)), True, True, 10, 4, 0, True, False, [1, 2, 3], '012', [0, 1, 2], (0, 1, 2), [2, 1, 0], 0, [0, 1, 2, 'e'], [0, 0], [0, 0], {0: None, 1: None, 2: None}, [0, 0, 1, 2], [0, 0, 1, 2], {0, 1, 2}, b'\\x00\\x01\\x02', bytearray(b'\\x00\\x01\\x02'), [0, 2, 4], frozenset({0, 1, 2}), '[0, 1]', 0, [(0, 0), (1, 1)], 2, 4, [1, 2, 3], [0, 1, 2], True]"]}, {"src": "def g(n):\n for i in range(n): yield i\nclass It:\n def __iter__(self): return g(3)\nclass Gi:\n def __iter__(self):\n yield 1\n yield 2\nout = []\ndef t(name, f):\n try: out.append((name, f()))\n except Exception as e: out.append((name, type(e).__name__))\nt('enum', lambda: tuple(enumerate(g(3))))\nt('zipstar', lambda: list(zip(*[g(2), g(2)])))\nt('bytes', lambda: bytes(g(3)))\nt('bytearray', lambda: bytearray(g(3)))\nt('mapnext', lambda: list(map(next, [g(2), g(3)])))\nt('join', lambda: '-'.join(map(str, g(3))))\nt('strjoin', lambda: ','.join(x for x in ['a', 'b']))\nt('listIt', lambda: list(It()))\nt('tupleGi', lambda: tuple(Gi()))\nt('sumIt', lambda: sum(It()))\nt('zipIt', lambda: list(zip(It(), Gi())))\nt('dictzip', lambda: dict(zip(g(2), Gi())))\nt('maxIt', lambda: max(It()))\nt('inIt', lambda: 2 in It())\nt('starIt', lambda: [*It()])\nt('unpack', lambda: (lambda a, b, c: a + b + c)(*g(3)))\nt('fmt', lambda: '%s %s' % tuple(g(2)))\nt('frozen', lambda: frozenset(It()))\nt('setupd', lambda: (lambda s: (s.update(It()), s)[1])(set()))\nt('listext', lambda: (lambda l: (l.extend(It()), l)[1])([]))\nt('dictupd', lambda: (lambda d: (d.update(zip(It(), g(3))), d)[1])({}))\nt('enumIt', lambda: list(enumerate(It(), 1)))\nt('filterIt', lambda: list(filter(None, It())))\nt('mapIt', lambda: list(map(lambda x: x * 2, It())))\nt('revlist', lambda: list(reversed([*g(3)])))\nt('anyIt', lambda: any(It()))\nt('allgen', lambda: all(x < 5 for x in It()))\nt('minkey', lambda: min(It(), key=lambda v: -v))\nt('nextIt', lambda: next(iter(It())))\nt('iterg', lambda: list(iter(g(2))))\nt('chain', lambda: [y for x in [g(2), g(2)] for y in x])\nt('print', lambda: print(*g(3), sep=','))\nt('str', lambda: str(list(g(2))))\nt('lenlist', lambda: len(list(It())))\nt('tuplemap', lambda: tuple(map(lambda a, b: a * b, g(3), It())))\nt('dictcomp', lambda: {k: v for k, v in zip(g(2), g(2))})\nt('setcomp', lambda: {x for x in It()})\nt('sortedmap', lambda: sorted(map(lambda x: -x, It())))\nt('countgen', lambda: [1, 2, 1].count(next(g(2)) + 1))\nprint(out)", "output": ["0,1,2", "[('enum', ((0, 0), (1, 1), (2, 2))), ('zipstar', [(0, 0), (1, 1)]), ('bytes', b'\\x00\\x01\\x02'), ('bytearray', bytearray(b'\\x00\\x01\\x02')), ('mapnext', [0, 0]), ('join', '0-1-2'), ('strjoin', 'a,b'), ('listIt', [0, 1, 2]), ('tupleGi', (1, 2)), ('sumIt', 3), ('zipIt', [(0, 1), (1, 2)]), ('dictzip', {0: 1, 1: 2}), ('maxIt', 2), ('inIt', True), ('starIt', [0, 1, 2]), ('unpack', 3), ('fmt', '0 1'), ('frozen', frozenset({0, 1, 2})), ('setupd', {0, 1, 2}), ('listext', [0, 1, 2]), ('dictupd', {0: 0, 1: 1, 2: 2}), ('enumIt', [(1, 0), (2, 1), (3, 2)]), ('filterIt', [1, 2]), ('mapIt', [0, 2, 4]), ('revlist', [2, 1, 0]), ('anyIt', True), ('allgen', True), ('minkey', 2), ('nextIt', 0), ('iterg', [0, 1]), ('chain', [0, 1, 0, 1]), ('print', None), ('str', '[0, 1]'), ('lenlist', 3), ('tuplemap', (0, 1, 4)), ('dictcomp', {0: 0, 1: 1}), ('setcomp', {0, 1, 2}), ('sortedmap', [-2, -1, 0]), ('countgen', 2)]"]}, {"src": "class K:\n made = 0\n def __init__(self, v):\n K.made += 1\n self.v = v\n def __lt__(self, o): return self.v < o.v\n def __gt__(self, o): return self.v > o.v\nxs = [5, 3, 9, 1, 7] * 10\nprint(max(xs, key=K), min(xs, key=K), K.made)\nprint(max(iter(xs), key=K), min((x for x in xs), key=K), K.made)\nprint(max(4, 8, 2, key=K), min([], key=K, default='none'))", "output": ["9 1 100", "9 1 200", "8 none"]}, + {"src": "B = 2**70\ns, b, l, t = \"abc\", b\"abc\", [1, 2, 3], (1, 2, 3)\nprint(repr(s[B:]), repr(s[-B::-1]), repr(s[:B]), repr(s[-B:]), repr(s[::B]), repr(s[B::-1]))\nprint(b[B:], b[-B::-1], l[B:], l[-B::-1], t[:-B], t[-B:B])\nprint(s.find(\"c\", B), s.find(\"a\", -B), s.rfind(\"a\", 0, -B), s.count(\"a\", -B, B), s.startswith(\"a\", -B), s.endswith(\"c\", 0, B))\nprint(b.find(b\"c\", B), b.find(b\"a\", -B), b.count(b\"a\", -B, B), b.startswith(b\"a\", -B), b.index(b\"a\", -B, B))\nprint(l.index(3, -B, B), t.index(1, -B))\nprint(slice(-B, B, 1).indices(3), range(10)[B:], range(10)[-B:3], memoryview(b\"abcd\")[-B:B].tobytes())\nfor f in (lambda: s[B], lambda: l[-B], lambda: b[B], lambda: s.index(\"c\", B), lambda: l.index(1, B)):\n try: print(f())\n except Exception as e: print(type(e).__name__, e)\nprint(repr(s[True:]), l[False:True], s.find(\"b\", True))", "output": ["'' '' 'abc' 'abc' 'a' 'cba'", "b'' b'' [] [] () (1, 2, 3)", "-1 0 -1 1 True True", "-1 0 1 True 0", "2 0", "(0, 3, 1) range(10, 10) range(0, 3) b'abcd'", "IndexError cannot fit 'int' into an index-sized integer", "IndexError cannot fit 'int' into an index-sized integer", "IndexError cannot fit 'int' into an index-sized integer", "ValueError substring not found", "ValueError list.index(x): x not in list", "'bc' [1] 1"]}, + {"src": "B = 2**70\nl = [1, 2, 3]\nfor f in (lambda: l.insert(B, 9), lambda: l.pop(B), lambda: l.pop(-B), lambda: bytearray(b'ab').pop(B), lambda: (1, 2)[B], lambda: range(3)[B], lambda: 'abc'[-B], lambda: bytearray(b'ab').insert(B, 1)):\n try: print(f())\n except Exception as e: print(type(e).__name__, e)\nprint(list(range(5))[::-B], 'abcd'[1:B:2], [1, 2, 3][B:-B:-1], b'abc'[B:-B:-1])", "output": ["OverflowError Python int too large to convert to C ssize_t", "OverflowError Python int too large to convert to C ssize_t", "OverflowError Python int too large to convert to C ssize_t", "OverflowError Python int too large to convert to C ssize_t", "IndexError cannot fit 'int' into an index-sized integer", "IndexError range object index out of range", "IndexError cannot fit 'int' into an index-sized integer", "OverflowError Python int too large to convert to C ssize_t", "[4] bd [3, 2, 1] b'cba'"]}, + {"src": "B = 2**70\nfor f in (lambda: 'a'.center(B), lambda: 'a'.ljust(B), lambda: 'a'.rjust(-B), lambda: 'a'.zfill(B), lambda: b'a'.center(B), lambda: b'a'.zfill(-B), lambda: 'a\\tb'.expandtabs(B), lambda: bytearray(b'a').ljust(B)):\n try: print(repr(f()))\n except Exception as e: print(type(e).__name__, e)", "output": ["OverflowError Python int too large to convert to C ssize_t", "OverflowError Python int too large to convert to C ssize_t", "OverflowError Python int too large to convert to C ssize_t", "OverflowError Python int too large to convert to C ssize_t", "OverflowError Python int too large to convert to C ssize_t", "OverflowError Python int too large to convert to C ssize_t", "OverflowError Python int too large to convert to C int", "OverflowError Python int too large to convert to C ssize_t"]}, {"src": "# min and max with an eval key over a generator or an instance gather the items as frames of the loop, each key a floor\ndef gen():\n yield '3'\n yield '1'\nclass U:\n def __iter__(self):\n return iter(['4', '9'])\nprint(max(gen(), key=eval), min(gen(), key=eval), max(U(), key=eval), min(iter([]), key=eval, default='d'))", "output": ["3 1 9 d"]}, {"src": "def gen():\n for s in ['1', '0', '2', '0', '0', '3']:\n yield s\ndef nums():\n for v in [0, 1, 0, 2, '', 'a']:\n yield v\nprint(list(filter(int, gen())))\nout = []\nfor x in filter(int, gen()):\n out.append(x)\nprint(out)\nprint(list(filter(bool, nums())), [y for y in filter(len, ['', 'a', '', 'bc'])], list(filter(None, nums())))\nt = 0\nfor k in range(50):\n for v in filter(int, gen()):\n t += int(v)\nprint(t, sum(filter(int, gen())) if False else 0, tuple(filter(str.isdigit, gen())))", "output": ["['1', '2', '3']", "['1', '2', '3']", "[1, 2, 'a'] ['a', 'bc'] [1, 2, 'a']", "300 0 ('1', '0', '2', '0', '0', '3')"]}, {"src": "# a flat frame whose hot loop lays a callee with a try catches what a frame it calls there raises\nclass W:\n def __await__(self):\n return (yield self)\nasync def tiny(i):\n await W()\n return i\ndef g():\n yield 1\ndef f(i):\n if i % 2:\n raise ValueError(i)\n return i\nclass L:\n def drive(self, ready):\n out = []\n k = 0\n while k < len(ready):\n self.step(ready[k], out)\n k += 1\n return out\n def step(self, c, out):\n try:\n c.send(None)\n except StopIteration as e:\n out.append(e.value)\n def pick(self, xs):\n out = []\n k = 0\n while k < len(xs):\n self.one(xs[k], out)\n k += 1\n return out\n def one(self, x, out):\n try:\n next(g())\n f(x)\n except ValueError as e:\n out.append(e.args[0])\ncs = [tiny(i) for i in range(12)]\nprint(L().drive(cs + cs))\nprint(L().pick(list(range(40))))", "output": ["[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]", "[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39]"]}, diff --git a/tests/common.rs b/tests/common.rs index edcc47e9..071875e2 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -8,8 +8,8 @@ use compiler::modules::{ NativeBinding, Resolved, Resolver, partition_bindings, Manifest, walk_up_dirs, dir_of, join_relative, }; -use compiler::vm::types::{HeapObj, HeapPool, Limits, Val, VmErr}; -use compiler::vm::VM; +use compiler::value::{HeapObj, HeapPool, Limits, Val, VmErr}; +use compiler::embed::VM; /* Answers deferred call `id` with text, as the wasm host does through `set_host_result_by_id`. */ pub fn answer(vm: &mut VM, id: u64, text: &str) -> bool { diff --git a/tests/coverage.rs b/tests/coverage.rs index a185f5ea..138e8224 100644 --- a/tests/coverage.rs +++ b/tests/coverage.rs @@ -1,8 +1,8 @@ #[cfg(feature = "coverage")] mod test { use compiler::lexer::lex; - use compiler::parser::{ImportKind, OpCode, Parser, SSAChunk, Value}; - use compiler::vm::VM; + use compiler::code::{ImportKind, OpCode, Parser, SSAChunk, Value}; + use compiler::embed::VM; use crate::common::TestResolver; diff --git a/tests/embed.rs b/tests/embed.rs index cce601b0..87f3e5cf 100644 --- a/tests/embed.rs +++ b/tests/embed.rs @@ -1,8 +1,9 @@ /* The VM calls a Rust embedder builds on, the site runs programs through exactly these, so none reads as dead here. */ mod test { use compiler::lexer::lex; - use compiler::parser::Parser; - use compiler::vm::{HeapObj, Limits, VM}; + use compiler::code::Parser; + use compiler::value::HeapObj; +use compiler::embed::{Limits, VM}; use crate::common::TestResolver; @@ -27,7 +28,7 @@ mod test { assert_eq!(&vm.output_text()[booted.len()..], "hi ada\n"); // A failing call renders a traceback that names the function it failed in. - let err = vm.call_export("greet", &[compiler::vm::Val::int(7)]).unwrap_err(); + let err = vm.call_export("greet", &[compiler::value::Val::int(7)]).unwrap_err(); let traceback = vm.render_error(&err, SRC, None); assert!(traceback.contains("TypeError") && traceback.contains("greet"), "{traceback}"); } diff --git a/tests/main.rs b/tests/main.rs index 360f057f..2a50faad 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -15,6 +15,8 @@ mod embed; mod memory; mod nested; mod tables; +#[cfg(feature = "pure")] +mod pure; /* cases/pending/asyncio.json holds the cases written for the async builtins the engine used to ship, `run`, `sleep`, `gather`, `with_timeout` and `cancel`, grouped by the suite each came from: vm.json, snapshot.json, modules.json @@ -23,6 +25,6 @@ mod tables; loads this file and nothing in it runs. The cases come back once asyncio arrives transpiled with the rest of the standard library, each one rewritten from the old builtins to asyncio (`asyncio.run`, `asyncio.sleep`, `asyncio.gather`, `asyncio.wait_for`, `Task.cancel`) and moved back into the suite it came from, which is how - they keep checking interleaving, cancellation, timeouts and host calls that wait inside a task. Three Rust tests - of vm.rs went with them, a gather child whose host call fails pointing at its own call and leaving no stale + they keep checking interleaving, cancellation, timeouts and host calls that wait inside a task. Three Rust tests + of vm.rs went with them, a gather child whose host call fails pointing at its own call and leaving no stale position once caught, and a host call answered inside a `with_timeout`, and come back the same way. */ diff --git a/tests/memory.rs b/tests/memory.rs index 9016cde4..d386cb27 100644 --- a/tests/memory.rs +++ b/tests/memory.rs @@ -1,10 +1,10 @@ /* What the meter reads for a program, every byte the allocator handed its run and took back. */ mod test { use compiler::lexer::lex; - use compiler::parser::Parser; - use compiler::parser::SSAChunk; + use compiler::code::Parser; + use compiler::code::SSAChunk; use compiler::value::meter::{Meter, set_budget}; - use compiler::vm::{Limits, VM}; + use compiler::embed::{Limits, VM}; fn compiled(src: &str) -> SSAChunk { let (tokens, _) = lex(src); diff --git a/tests/modules.rs b/tests/modules.rs index 923f38dc..4df0c421 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -3,9 +3,9 @@ mod test { use std::collections::HashMap; use compiler::lexer::lex; - use compiler::parser::Parser; - use compiler::vm::VM; - use compiler::vm::types::{SchedulerStatus, VmErr}; + use compiler::code::Parser; + use compiler::embed::VM; + use compiler::value::{SchedulerStatus, VmErr}; use compiler::modules::{NativeBinding, Resolver}; use crate::common::{answer, fail, TestResolver, test_native}; diff --git a/tests/nested.rs b/tests/nested.rs index 1d54af8e..948056ca 100644 --- a/tests/nested.rs +++ b/tests/nested.rs @@ -3,8 +3,8 @@ #[test] fn old_path_ceilings() { use compiler::lexer::lex; - use compiler::parser::Parser; - use compiler::vm::{Limits, VM}; + use compiler::code::Parser; + use compiler::embed::{Limits, VM}; const CEILINGS: [u64; 1] = [0]; const NAMES: [&str; 1] = ["generic frames"]; let cases: Vec = serde_json::from_str(&std::fs::read_to_string("tests/cases/vm.json").unwrap()).unwrap(); @@ -15,7 +15,7 @@ fn old_path_ceilings() { let src = case["src"].as_str().unwrap(); let (tokens, _) = lex(src); let (mut chunk, _) = Parser::new(src, tokens.into_iter()).parse(); - compiler::vm::optimizer::constant_fold(&mut chunk); + compiler::code::constant_fold(&mut chunk); let mut vm = VM::with_limits(&chunk, Limits::sandbox()); let _ = vm.run(); for (t, n) in total.iter_mut().zip(vm.old_path_counts()) { *t += n; } diff --git a/tests/parser.rs b/tests/parser.rs index 010f4b0d..30c2238b 100644 --- a/tests/parser.rs +++ b/tests/parser.rs @@ -2,7 +2,7 @@ mod test { use compiler::lexer::lex; - use compiler::parser::{Parser, Value, Diagnostic}; + use compiler::code::{Parser, Value, Diagnostic}; #[derive(serde::Deserialize)] #[serde(deny_unknown_fields)] diff --git a/tests/pure.rs b/tests/pure.rs new file mode 100644 index 00000000..e6755950 --- /dev/null +++ b/tests/pure.rs @@ -0,0 +1,362 @@ +mod test { + use compiler::value::BigInt; + use compiler::pure::{Plain, Raised, call_pure}; + + fn s(x: &str) -> Plain { Plain::Str(x.into()) } + fn b(x: &[u8]) -> Plain { Plain::Bytes(x.to_vec()) } + fn big(x: &str) -> Plain { let d: Vec = x.trim_start_matches('-').bytes().map(|c| c - b'0').collect(); Plain::Big(BigInt::from_digits(x.starts_with('-'), &d, 10)) } + + // A call and what python3 gave for it. + type Case = (&'static str, Vec, Result); + + fn cases() -> Vec { + vec![ + ("str.upper", vec![s("abc ß")], Ok(s("ABC SS"))), + ("str.lower", vec![s("ÀB C")], Ok(s("àb c"))), + ("str.casefold", vec![s("ÀbC")], Ok(s("àbc"))), + ("str.casefold", vec![s("Straße fi ᾈ")], Ok(s("strasse fi ἀι"))), + ("str.capitalize", vec![s("hELLO world")], Ok(s("Hello world"))), + ("str.title", vec![s("they're bill's 1st")], Ok(s("They'Re Bill'S 1St"))), + ("str.swapcase", vec![s("Hello ÉÈ")], Ok(s("hELLO éè"))), + ("str.strip", vec![s(" hi \u{9}\u{a}")], Ok(s("hi"))), + ("str.strip", vec![s("xxhixx"), s("x")], Ok(s("hi"))), + ("str.lstrip", vec![s(" hi ")], Ok(s("hi "))), + ("str.rstrip", vec![s("abcxyx"), s("xy")], Ok(s("abc"))), + ("str.find", vec![s("héllo Wörld"), s("o")], Ok(Plain::Int(4))), + ("str.find", vec![s("héllo Wörld"), s("o"), Plain::Int(5)], Ok(Plain::Int(-1))), + ("str.find", vec![s("héllo Wörld"), s("o"), Plain::Int(-3)], Ok(Plain::Int(-1))), + ("str.find", vec![s("héllo Wörld"), s("zz")], Ok(Plain::Int(-1))), + ("str.rfind", vec![s("héllo Wörld"), s("o")], Ok(Plain::Int(4))), + ("str.find", vec![s("abc"), s(""), Plain::Int(5)], Ok(Plain::Int(-1))), + ("str.index", vec![s("héllo Wörld"), s("W")], Ok(Plain::Int(6))), + ("str.index", vec![s("abc"), s("z")], Err(("ValueError", "substring not found"))), + ("str.rindex", vec![s("abcabc"), s("b"), Plain::Int(0), Plain::Int(4)], Ok(Plain::Int(1))), + ("str.count", vec![s("banana"), s("an")], Ok(Plain::Int(2))), + ("str.count", vec![s("banana"), s("")], Ok(Plain::Int(7))), + ("str.count", vec![s("banana"), s("a"), Plain::Int(2), Plain::Int(-1)], Ok(Plain::Int(1))), + ("str.replace", vec![s("aaaa"), s("a"), s("bb")], Ok(s("bbbbbbbb"))), + ("str.replace", vec![s("aaaa"), s("a"), s("b"), Plain::Int(2)], Ok(s("bbaa"))), + ("str.replace", vec![s("abc"), s(""), s("-")], Ok(s("-a-b-c-"))), + ("str.startswith", vec![s("héllo Wörld"), s("hé")], Ok(Plain::Bool(true))), + ("str.startswith", vec![s("héllo Wörld"), Plain::Tuple(vec![s("x"), s("hél")])], Ok(Plain::Bool(true))), + ("str.startswith", vec![s("héllo Wörld"), s("llo"), Plain::Int(2)], Ok(Plain::Bool(true))), + ("str.endswith", vec![s("héllo Wörld"), s("rld"), Plain::Int(0), Plain::Int(20)], Ok(Plain::Bool(true))), + ("str.endswith", vec![s("abc"), Plain::Tuple(vec![s("b")]), Plain::Int(0), Plain::Int(2)], Ok(Plain::Bool(true))), + ("str.startswith", vec![s("abc"), Plain::Int(1)], Err(("TypeError", "startswith first arg must be str or a tuple of str, not int"))), + ("str.split", vec![s("a b c ")], Ok(Plain::List(vec![s("a"), s("b"), s("c")]))), + ("str.split", vec![s("a,b,,c"), s(",")], Ok(Plain::List(vec![s("a"), s("b"), s(""), s("c")]))), + ("str.split", vec![s("a,b,c"), s(","), Plain::Int(1)], Ok(Plain::List(vec![s("a"), s("b,c")]))), + ("str.split", vec![s(" a b c "), Plain::None, Plain::Int(1)], Ok(Plain::List(vec![s("a"), s("b c ")]))), + ("str.rsplit", vec![s("a b c"), Plain::None, Plain::Int(1)], Ok(Plain::List(vec![s("a b"), s("c")]))), + ("str.rsplit", vec![s("a,b,c"), s(","), Plain::Int(1)], Ok(Plain::List(vec![s("a,b"), s("c")]))), + ("str.split", vec![s("abc"), s("")], Err(("ValueError", "empty separator"))), + ("str.splitlines", vec![s("a\u{a}b\u{d}\u{a}c\u{d}d\u{b}e")], Ok(Plain::List(vec![s("a"), s("b"), s("c"), s("d"), s("e")]))), + ("str.splitlines", vec![s("a\u{a}b\u{a}"), Plain::Bool(true)], Ok(Plain::List(vec![s("a\u{a}"), s("b\u{a}")]))), + ("str.partition", vec![s("a=b=c"), s("=")], Ok(Plain::Tuple(vec![s("a"), s("="), s("b=c")]))), + ("str.partition", vec![s("abc"), s("=")], Ok(Plain::Tuple(vec![s("abc"), s(""), s("")]))), + ("str.rpartition", vec![s("a=b=c"), s("=")], Ok(Plain::Tuple(vec![s("a=b"), s("="), s("c")]))), + ("str.rpartition", vec![s("abc"), s("=")], Ok(Plain::Tuple(vec![s(""), s(""), s("abc")]))), + ("str.join", vec![s(", "), Plain::List(vec![s("a"), s("b"), s("c")])], Ok(s("a, b, c"))), + ("str.join", vec![s("-"), s("xyz")], Ok(s("x-y-z"))), + ("str.join", vec![s("-"), Plain::Tuple(vec![s("a")])], Ok(s("a"))), + ("str.join", vec![s("-"), Plain::List(vec![s("a"), Plain::Int(1)])], Err(("TypeError", "sequence item 1: expected str instance, int found"))), + ("str.center", vec![s("ab"), Plain::Int(7), s("*")], Ok(s("***ab**"))), + ("str.center", vec![s("abc"), Plain::Int(6)], Ok(s(" abc "))), + ("str.ljust", vec![s("ab"), Plain::Int(5)], Ok(s("ab "))), + ("str.rjust", vec![s("ab"), Plain::Int(5), s("0")], Ok(s("000ab"))), + ("str.zfill", vec![s("-42"), Plain::Int(6)], Ok(s("-00042"))), + ("str.zfill", vec![s("abc"), Plain::Int(2)], Ok(s("abc"))), + ("str.center", vec![s("a"), Plain::Int(3), s("ab")], Err(("TypeError", "The fill character must be exactly one character long"))), + ("str.removeprefix", vec![s("prefix-x"), s("prefix-")], Ok(s("x"))), + ("str.removesuffix", vec![s("a.py"), s(".py")], Ok(s("a"))), + ("str.removesuffix", vec![s("a.py"), s(".rs")], Ok(s("a.py"))), + ("str.expandtabs", vec![s("a\u{9}bc\u{9}d")], Ok(s("a bc d"))), + ("str.expandtabs", vec![s("a\u{9}b"), Plain::Int(4)], Ok(s("a b"))), + ("str.isdigit", vec![s("123")], Ok(Plain::Bool(true))), + ("str.isdigit", vec![s("²")], Ok(Plain::Bool(true))), + ("str.isdecimal", vec![s("²")], Ok(Plain::Bool(false))), + ("str.isnumeric", vec![s("½")], Ok(Plain::Bool(true))), + ("str.isalpha", vec![s("abcé")], Ok(Plain::Bool(true))), + ("str.isalnum", vec![s("ab1")], Ok(Plain::Bool(true))), + ("str.isspace", vec![s(" \u{9}")], Ok(Plain::Bool(true))), + ("str.isspace", vec![s("")], Ok(Plain::Bool(false))), + ("str.isupper", vec![s("ABC1")], Ok(Plain::Bool(true))), + ("str.islower", vec![s("abc")], Ok(Plain::Bool(true))), + ("str.istitle", vec![s("Hello World")], Ok(Plain::Bool(true))), + ("str.istitle", vec![s("Hello world")], Ok(Plain::Bool(false))), + ("str.isascii", vec![s("abc")], Ok(Plain::Bool(true))), + ("str.isascii", vec![s("é")], Ok(Plain::Bool(false))), + ("str.isprintable", vec![s("a\u{a}")], Ok(Plain::Bool(false))), + ("str.isidentifier", vec![s("_a1")], Ok(Plain::Bool(true))), + ("str.isidentifier", vec![s("1a")], Ok(Plain::Bool(false))), + ("str.encode", vec![s("héllo")], Ok(b(b"h\xc3\xa9llo"))), + ("str.encode", vec![s("abc"), s("ascii")], Ok(b(b"abc"))), + ("str.maketrans", vec![s("ab"), s("xy")], Ok(Plain::Dict(vec![(Plain::Int(97), Plain::Int(120)), (Plain::Int(98), Plain::Int(121))]))), + ("bytes.upper", vec![b(b"abc")], Ok(b(b"ABC"))), + ("bytes.lower", vec![b(b"ABC")], Ok(b(b"abc"))), + ("bytes.split", vec![b(b"a b c")], Ok(Plain::List(vec![b(b"a"), b(b"b"), b(b"c")]))), + ("bytes.split", vec![b(b"a,b"), b(b",")], Ok(Plain::List(vec![b(b"a"), b(b"b")]))), + ("bytes.find", vec![b(b"hello"), b(b"l")], Ok(Plain::Int(2))), + ("bytes.find", vec![b(b"hello"), Plain::Int(108)], Ok(Plain::Int(2))), + ("bytes.rfind", vec![b(b"hello"), b(b"l")], Ok(Plain::Int(3))), + ("bytes.index", vec![b(b"abc"), b(b"z")], Err(("ValueError", "subsection not found"))), + ("bytes.count", vec![b(b"banana"), b(b"a")], Ok(Plain::Int(3))), + ("bytes.replace", vec![b(b"aaa"), b(b"a"), b(b"bb")], Ok(b(b"bbbbbb"))), + ("bytes.strip", vec![b(b" x ")], Ok(b(b"x"))), + ("bytes.strip", vec![b(b"xxyxx"), b(b"x")], Ok(b(b"y"))), + ("bytes.startswith", vec![b(b"hello"), b(b"he")], Ok(Plain::Bool(true))), + ("bytes.endswith", vec![b(b"hello"), Plain::Tuple(vec![b(b"x"), b(b"lo")])], Ok(Plain::Bool(true))), + ("bytes.join", vec![b(b"-"), Plain::List(vec![b(b"a"), b(b"b")])], Ok(b(b"a-b"))), + ("bytes.hex", vec![b(b"\x01\xab")], Ok(s("01ab"))), + ("bytes.hex", vec![b(b"\x01\xab\xcd"), s(":"), Plain::Int(2)], Ok(s("01:abcd"))), + ("bytes.decode", vec![b(b"h\xc3\xa9")], Ok(s("hé"))), + ("bytes.decode", vec![b(b"\xff"), s("utf-8"), s("replace")], Ok(s("�"))), + ("bytes.decode", vec![b(b"\xff")], Err(("UnicodeDecodeError", "'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"))), + ("bytes.partition", vec![b(b"a=b"), b(b"=")], Ok(Plain::Tuple(vec![b(b"a"), b(b"="), b(b"b")]))), + ("bytes.rsplit", vec![b(b"a b c"), Plain::None, Plain::Int(1)], Ok(Plain::List(vec![b(b"a b"), b(b"c")]))), + ("bytes.splitlines", vec![b(b"a\x0ab\x0d\x0a")], Ok(Plain::List(vec![b(b"a"), b(b"b")]))), + ("bytes.center", vec![b(b"ab"), Plain::Int(6), b(b"-")], Ok(b(b"--ab--"))), + ("bytes.zfill", vec![b(b"-1"), Plain::Int(4)], Ok(b(b"-001"))), + ("bytes.expandtabs", vec![b(b"a\x09b")], Ok(b(b"a b"))), + ("bytes.title", vec![b(b"hello world")], Ok(b(b"Hello World"))), + ("bytes.capitalize", vec![b(b"hELLO")], Ok(b(b"Hello"))), + ("bytes.swapcase", vec![b(b"aB")], Ok(b(b"Ab"))), + ("bytes.isdigit", vec![b(b"12")], Ok(Plain::Bool(true))), + ("bytes.isalpha", vec![b(b"ab1")], Ok(Plain::Bool(false))), + ("bytes.istitle", vec![b(b"Ab Cd")], Ok(Plain::Bool(true))), + ("bytes.removeprefix", vec![b(b"abc"), b(b"a")], Ok(b(b"bc"))), + ("bytes.fromhex", vec![s("01 ab")], Ok(b(b"\x01\xab"))), + ("bytes.maketrans", vec![b(b"ab"), b(b"xy")], Ok(b(b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\x22#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\x5c]^_`xycdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"))), + ("bytes.translate", vec![b(b"abc"), Plain::None, b(b"b")], Ok(b(b"ac"))), + ("list.index", vec![Plain::List(vec![Plain::Int(1), Plain::Int(2), Plain::Int(3), Plain::Int(2)]), Plain::Int(2)], Ok(Plain::Int(1))), + ("list.index", vec![Plain::List(vec![Plain::Int(1), Plain::Int(2), Plain::Int(3), Plain::Int(2)]), Plain::Int(2), Plain::Int(2)], Ok(Plain::Int(3))), + ("list.index", vec![Plain::List(vec![Plain::Int(1)]), Plain::Int(5)], Err(("ValueError", "list.index(x): x not in list"))), + ("list.count", vec![Plain::List(vec![Plain::Int(1), Plain::Int(2), Plain::Int(2), s("a")]), Plain::Int(2)], Ok(Plain::Int(2))), + ("list.copy", vec![Plain::List(vec![Plain::Int(1), Plain::List(vec![Plain::Int(2)]), s("x")])], Ok(Plain::List(vec![Plain::Int(1), Plain::List(vec![Plain::Int(2)]), s("x")]))), + ("tuple.index", vec![Plain::Tuple(vec![Plain::Int(1), Plain::Int(2)]), Plain::Int(2)], Ok(Plain::Int(1))), + ("tuple.count", vec![Plain::Tuple(vec![Plain::Int(1), Plain::Int(1), Plain::Int(2)]), Plain::Int(1)], Ok(Plain::Int(2))), + ("tuple.index", vec![Plain::Tuple(vec![Plain::Int(1)]), Plain::Int(9)], Err(("ValueError", "tuple.index(x): x not in tuple"))), + ("dict.get", vec![Plain::Dict(vec![(s("a"), Plain::Int(1))]), s("a")], Ok(Plain::Int(1))), + ("dict.get", vec![Plain::Dict(vec![(s("a"), Plain::Int(1))]), s("b")], Ok(Plain::None)), + ("dict.get", vec![Plain::Dict(vec![(s("a"), Plain::Int(1))]), s("b"), Plain::Int(0)], Ok(Plain::Int(0))), + ("dict.get", vec![Plain::Dict(vec![]), Plain::List(vec![])], Err(("TypeError", "cannot use 'list' as a dict key (unhashable type: 'list')"))), + ("dict.copy", vec![Plain::Dict(vec![(s("a"), Plain::Int(1)), (Plain::Int(2), Plain::Tuple(vec![Plain::Int(3)]))])], Ok(Plain::Dict(vec![(s("a"), Plain::Int(1)), (Plain::Int(2), Plain::Tuple(vec![Plain::Int(3)]))]))), + ("dict.fromkeys", vec![Plain::List(vec![s("a"), s("b")])], Ok(Plain::Dict(vec![(s("a"), Plain::None), (s("b"), Plain::None)]))), + ("dict.fromkeys", vec![s("ab"), Plain::Int(0)], Ok(Plain::Dict(vec![(s("a"), Plain::Int(0)), (s("b"), Plain::Int(0))]))), + ("dict.fromkeys", vec![Plain::List(vec![Plain::List(vec![Plain::Int(1)])])], Err(("TypeError", "cannot use 'list' as a dict key (unhashable type: 'list')"))), + ("set.union", vec![Plain::Set(vec![Plain::Int(1), Plain::Int(2)]), Plain::List(vec![Plain::Int(2), Plain::Int(3)])], Ok(Plain::Set(vec![Plain::Int(1), Plain::Int(2), Plain::Int(3)]))), + ("set.intersection", vec![Plain::Set(vec![Plain::Int(1), Plain::Int(2), Plain::Int(3)]), Plain::Tuple(vec![Plain::Int(2), Plain::Int(3), Plain::Int(4)])], Ok(Plain::Set(vec![Plain::Int(2), Plain::Int(3)]))), + ("set.difference", vec![Plain::Set(vec![Plain::Int(1), Plain::Int(2), Plain::Int(3)]), Plain::Set(vec![Plain::Int(2)])], Ok(Plain::Set(vec![Plain::Int(1), Plain::Int(3)]))), + ("set.symmetric_difference", vec![Plain::Set(vec![Plain::Int(1), Plain::Int(2)]), Plain::Set(vec![Plain::Int(2), Plain::Int(3)])], Ok(Plain::Set(vec![Plain::Int(1), Plain::Int(3)]))), + ("set.issubset", vec![Plain::Set(vec![Plain::Int(1)]), Plain::List(vec![Plain::Int(1), Plain::Int(2)])], Ok(Plain::Bool(true))), + ("set.issuperset", vec![Plain::Set(vec![Plain::Int(1), Plain::Int(2)]), Plain::Set(vec![Plain::Int(3)])], Ok(Plain::Bool(false))), + ("set.isdisjoint", vec![Plain::Set(vec![Plain::Int(1)]), Plain::Set(vec![Plain::Int(2)])], Ok(Plain::Bool(true))), + ("set.copy", vec![Plain::Set(vec![s("a"), Plain::Int(1)])], Ok(Plain::Set(vec![s("a"), Plain::Int(1)]))), + ("frozenset.union", vec![Plain::FrozenSet(vec![Plain::Int(1)]), Plain::Set(vec![Plain::Int(2)])], Ok(Plain::FrozenSet(vec![Plain::Int(1), Plain::Int(2)]))), + ("frozenset.copy", vec![Plain::FrozenSet(vec![Plain::Int(3)])], Ok(Plain::FrozenSet(vec![Plain::Int(3)]))), + ("set.union", vec![Plain::Set(vec![Plain::Int(1)]), Plain::List(vec![Plain::List(vec![Plain::Int(1)])])], Err(("TypeError", "cannot use 'list' as a set element (unhashable type: 'list')"))), + ("int.bit_length", vec![Plain::Int(255)], Ok(Plain::Int(8))), + ("int.bit_length", vec![big("-1180591620717411303424")], Ok(Plain::Int(71))), + ("int.bit_count", vec![Plain::Int(7)], Ok(Plain::Int(3))), + ("int.to_bytes", vec![Plain::Int(258), Plain::Int(2), s("big")], Ok(b(b"\x01\x02"))), + ("int.to_bytes", vec![Plain::Int(256), Plain::Int(1), s("big")], Err(("OverflowError", "int too big to convert"))), + ("int.from_bytes", vec![b(b"\x01\x02"), s("big")], Ok(Plain::Int(258))), + ("int.as_integer_ratio", vec![Plain::Int(6)], Ok(Plain::Tuple(vec![Plain::Int(6), Plain::Int(1)]))), + ("int.is_integer", vec![Plain::Int(3)], Ok(Plain::Bool(true))), + ("float.is_integer", vec![Plain::Float(2.0)], Ok(Plain::Bool(true))), + ("float.is_integer", vec![Plain::Float(2.5)], Ok(Plain::Bool(false))), + ("float.as_integer_ratio", vec![Plain::Float(0.75)], Ok(Plain::Tuple(vec![Plain::Int(3), Plain::Int(4)]))), + ("float.as_integer_ratio", vec![Plain::Float(f64::INFINITY)], Err(("OverflowError", "cannot convert Infinity to integer ratio"))), + ("float.hex", vec![Plain::Float(1.5)], Ok(s("0x1.8000000000000p+0"))), + ("float.hex", vec![Plain::Float(-0.1)], Ok(s("-0x1.999999999999ap-4"))), + ("float.fromhex", vec![s("0x1.8p1")], Ok(Plain::Float(3.0))), + ("float.fromhex", vec![s("zz")], Err(("ValueError", "invalid hexadecimal floating-point string"))), + ("range.index", vec![Plain::Range(0, 10, 2), Plain::Int(4)], Ok(Plain::Int(2))), + ("range.index", vec![Plain::Range(0, 5, 1), Plain::Int(9)], Err(("ValueError", "range.index(x): x not in range"))), + ("range.count", vec![Plain::Range(0, 5, 1), Plain::Int(3)], Ok(Plain::Int(1))), + ("slice.indices", vec![Plain::Slice(Box::new([Plain::None, Plain::None, Plain::Int(-1)])), Plain::Int(5)], Ok(Plain::Tuple(vec![Plain::Int(4), Plain::Int(-1), Plain::Int(-1)]))), + ("slice.indices", vec![Plain::Slice(Box::new([Plain::Int(1), Plain::Int(100), Plain::Int(2)])), Plain::Int(10)], Ok(Plain::Tuple(vec![Plain::Int(1), Plain::Int(10), Plain::Int(2)]))), + ("slice.indices", vec![Plain::Slice(Box::new([Plain::Int(0), Plain::Int(1), Plain::Int(0)])), Plain::Int(3)], Err(("ValueError", "slice step cannot be zero"))), + ("str.__getitem__", vec![s("héllo Wörld"), Plain::Int(1)], Ok(s("é"))), + ("str.__getitem__", vec![s("héllo Wörld"), Plain::Int(-1)], Ok(s("d"))), + ("str.__getitem__", vec![s("héllo Wörld"), Plain::Int(99)], Err(("IndexError", "string index out of range"))), + ("str.__getitem__", vec![s("abc"), Plain::Bool(true)], Ok(s("b"))), + ("str.__getitem__", vec![s("héllo Wörld"), Plain::Slice(Box::new([Plain::None, Plain::None, Plain::Int(-1)]))], Ok(s("dlröW olléh"))), + ("str.__getitem__", vec![s("héllo Wörld"), Plain::Slice(Box::new([Plain::Int(1), Plain::Int(8), Plain::Int(3)]))], Ok(s("éoö"))), + ("str.__getitem__", vec![s("héllo Wörld"), Plain::Slice(Box::new([Plain::Int(-2), Plain::Int(-9), Plain::Int(-2)]))], Ok(s("lö l"))), + ("str.__getitem__", vec![s("abcdef"), Plain::Slice(Box::new([Plain::None, Plain::None, Plain::Int(-2)]))], Ok(s("fdb"))), + ("str.__getitem__", vec![s("abc"), s("x")], Err(("TypeError", "string indices must be integers, not 'str'"))), + ("str.__len__", vec![s("héllo Wörld")], Ok(Plain::Int(11))), + ("str.__contains__", vec![s("héllo Wörld"), s("Wö")], Ok(Plain::Bool(true))), + ("str.__contains__", vec![s("abc"), Plain::Int(1)], Err(("TypeError", "'in ' requires string as left operand, not int"))), + ("str.__repr__", vec![s("it's")], Ok(s("\u{22}it's\u{22}"))), + ("str.__str__", vec![s("x")], Ok(s("x"))), + ("bytes.__getitem__", vec![b(b"abc"), Plain::Int(0)], Ok(Plain::Int(97))), + ("bytes.__getitem__", vec![b(b"abcdef"), Plain::Slice(Box::new([Plain::None, Plain::None, Plain::Int(-2)]))], Ok(b(b"fdb"))), + ("bytes.__contains__", vec![b(b"abc"), Plain::Int(98)], Ok(Plain::Bool(true))), + ("bytes.__repr__", vec![b(b"a'b\x00")], Ok(s("b\u{22}a'b\u{5c}x00\u{22}"))), + ("bytes.__len__", vec![b(b"ab")], Ok(Plain::Int(2))), + ("list.__getitem__", vec![Plain::List(vec![Plain::Int(1), Plain::Int(2), Plain::Int(3)]), Plain::Int(-1)], Ok(Plain::Int(3))), + ("list.__getitem__", vec![Plain::List(vec![Plain::Int(1), Plain::Int(2), Plain::Int(3), Plain::Int(4)]), Plain::Slice(Box::new([Plain::Int(3), Plain::Int(0), Plain::Int(-1)]))], Ok(Plain::List(vec![Plain::Int(4), Plain::Int(3), Plain::Int(2)]))), + ("list.__getitem__", vec![Plain::List(vec![Plain::Int(1)]), Plain::Int(5)], Err(("IndexError", "list index out of range"))), + ("tuple.__getitem__", vec![Plain::Tuple(vec![Plain::Int(1), Plain::Int(2), Plain::Int(3)]), Plain::Slice(Box::new([Plain::None, Plain::Int(2), Plain::None]))], Ok(Plain::Tuple(vec![Plain::Int(1), Plain::Int(2)]))), + ("tuple.__repr__", vec![Plain::Tuple(vec![Plain::Int(1)])], Ok(s("(1,)"))), + ("tuple.__contains__", vec![Plain::Tuple(vec![Plain::Int(1), Plain::List(vec![Plain::Int(2)])]), Plain::List(vec![Plain::Int(2)])], Ok(Plain::Bool(true))), + ("dict.__getitem__", vec![Plain::Dict(vec![(s("a"), Plain::Int(1))]), s("a")], Ok(Plain::Int(1))), + ("dict.__getitem__", vec![Plain::Dict(vec![(s("a"), Plain::Int(1))]), s("b")], Err(("KeyError", "'b'"))), + ("dict.__contains__", vec![Plain::Dict(vec![(s("a"), Plain::Int(1))]), s("a")], Ok(Plain::Bool(true))), + ("dict.__repr__", vec![Plain::Dict(vec![(s("a"), Plain::List(vec![Plain::Int(1), s("x")]))])], Ok(s("{'a': [1, 'x']}"))), + ("set.__contains__", vec![Plain::Set(vec![Plain::Int(1), Plain::Int(2)]), Plain::Int(2)], Ok(Plain::Bool(true))), + ("set.__len__", vec![Plain::Set(vec![Plain::Int(1), Plain::Int(2)])], Ok(Plain::Int(2))), + ("set.__contains__", vec![Plain::Set(vec![Plain::Int(1)]), Plain::List(vec![])], Err(("TypeError", "cannot use 'list' as a set element (unhashable type: 'list')"))), + ("frozenset.__repr__", vec![Plain::FrozenSet(vec![])], Ok(s("frozenset()"))), + ("int.__repr__", vec![big("1000000000000000000000000000000")], Ok(s("1000000000000000000000000000000"))), + ("float.__repr__", vec![Plain::Float(1e+22)], Ok(s("1e+22"))), + ("float.__str__", vec![Plain::Float(0.1)], Ok(s("0.1"))), + ("int.__bool__", vec![Plain::Int(0)], Ok(Plain::Bool(false))), + ("len", vec![s("héllo")], Ok(Plain::Int(5))), + ("len", vec![Plain::List(vec![Plain::Int(1), Plain::Int(2)])], Ok(Plain::Int(2))), + ("len", vec![Plain::Dict(vec![(Plain::Int(1), Plain::Int(2))])], Ok(Plain::Int(1))), + ("len", vec![Plain::Int(5)], Err(("TypeError", "object of type 'int' has no len()"))), + ("abs", vec![Plain::Int(-3)], Ok(Plain::Int(3))), + ("abs", vec![Plain::Float(-2.5)], Ok(Plain::Float(2.5))), + ("abs", vec![s("x")], Err(("TypeError", "bad operand type for abs(): 'str'"))), + ("str", vec![Plain::Int(12)], Ok(s("12"))), + ("str", vec![Plain::List(vec![Plain::Int(1), s("a")])], Ok(s("[1, 'a']"))), + ("str", vec![b(b"ab")], Ok(s("b'ab'"))), + ("str", vec![b(b"h\xc3\xa9"), s("utf-8")], Ok(s("hé"))), + ("str", vec![], Ok(s(""))), + ("repr", vec![s("a\u{a}b")], Ok(s("'a\u{5c}nb'"))), + ("repr", vec![Plain::Dict(vec![(s("k"), Plain::Tuple(vec![Plain::Int(1)]))])], Ok(s("{'k': (1,)}"))), + ("ascii", vec![s("é")], Ok(s("'\u{5c}xe9'"))), + ("int", vec![s(" 42 ")], Ok(Plain::Int(42))), + ("int", vec![s("ff"), Plain::Int(16)], Ok(Plain::Int(255))), + ("int", vec![s("0x1f"), Plain::Int(0)], Ok(Plain::Int(31))), + ("int", vec![Plain::Float(3.9)], Ok(Plain::Int(3))), + ("int", vec![s("x")], Err(("ValueError", "invalid literal for int() with base 10: 'x'"))), + ("int", vec![big("100000000000000000000")], Ok(big("100000000000000000000"))), + ("float", vec![s("1e3")], Ok(Plain::Float(1000.0))), + ("float", vec![s(" nan ")], Ok(Plain::Float(f64::NAN))), + ("float", vec![s("x")], Err(("ValueError", "could not convert string to float: 'x'"))), + ("bool", vec![Plain::List(vec![])], Ok(Plain::Bool(false))), + ("bool", vec![s("a")], Ok(Plain::Bool(true))), + ("chr", vec![Plain::Int(233)], Ok(s("é"))), + ("chr", vec![Plain::Int(-1)], Err(("ValueError", "chr() arg not in range(0x110000)"))), + ("ord", vec![s("é")], Ok(Plain::Int(233))), + ("ord", vec![s("ab")], Err(("TypeError", "ord() expected a character, but string of length 2 found"))), + ("range", vec![Plain::Int(5)], Ok(Plain::Range(0, 5, 1))), + ("range", vec![Plain::Int(1), Plain::Int(10), Plain::Int(3)], Ok(Plain::Range(1, 10, 3))), + ("range", vec![Plain::Int(1), Plain::Int(2), Plain::Int(0)], Err(("ValueError", "range() arg 3 must not be zero"))), + ("round", vec![Plain::Float(2.675), Plain::Int(2)], Ok(Plain::Float(2.67))), + ("round", vec![Plain::Float(2.5)], Ok(Plain::Int(2))), + ("round", vec![Plain::Int(1234), Plain::Int(-2)], Ok(Plain::Int(1200))), + ("bin", vec![Plain::Int(10)], Ok(s("0b1010"))), + ("oct", vec![Plain::Int(-8)], Ok(s("-0o10"))), + ("hex", vec![Plain::Int(255)], Ok(s("0xff"))), + ("hex", vec![big("1180591620717411303424")], Ok(s("0x400000000000000000"))), + ("callable", vec![Plain::Int(3)], Ok(Plain::Bool(false))), + ("format", vec![Plain::Float(2.71), s(".1f")], Ok(s("2.7"))), + ("format", vec![Plain::Int(42), s(">6")], Ok(s(" 42"))), + ("format", vec![s("ab"), s("^6")], Ok(s(" ab "))), + ("format", vec![Plain::Int(255), s("x")], Ok(s("ff"))), + ("slice", vec![Plain::Int(1), Plain::Int(5)], Ok(Plain::Slice(Box::new([Plain::Int(1), Plain::Int(5), Plain::None])))), + ("complex", vec![Plain::Int(1), Plain::Int(2)], Ok(Plain::Complex(1.0, 2.0))), + ("complex", vec![s("1+2j")], Ok(Plain::Complex(1.0, 2.0))), + ("list", vec![s("abc")], Ok(Plain::List(vec![s("a"), s("b"), s("c")]))), + ("list", vec![Plain::Range(0, 3, 1)], Ok(Plain::List(vec![Plain::Int(0), Plain::Int(1), Plain::Int(2)]))), + ("list", vec![Plain::Dict(vec![(s("a"), Plain::Int(1))])], Ok(Plain::List(vec![s("a")]))), + ("list", vec![Plain::Int(5)], Err(("TypeError", "'int' object is not iterable"))), + ("tuple", vec![Plain::List(vec![Plain::Int(1), Plain::Int(2)])], Ok(Plain::Tuple(vec![Plain::Int(1), Plain::Int(2)]))), + ("dict", vec![Plain::List(vec![Plain::Tuple(vec![s("a"), Plain::Int(1)]), Plain::Tuple(vec![s("b"), Plain::Int(2)])])], Ok(Plain::Dict(vec![(s("a"), Plain::Int(1)), (s("b"), Plain::Int(2))]))), + ("dict", vec![Plain::Dict(vec![(s("a"), Plain::Int(1))])], Ok(Plain::Dict(vec![(s("a"), Plain::Int(1))]))), + ("dict", vec![Plain::List(vec![Plain::Int(1)])], Err(("TypeError", "object is not iterable"))), + ("set", vec![s("aab")], Ok(Plain::Set(vec![s("a"), s("b")]))), + ("frozenset", vec![Plain::List(vec![Plain::Int(1), Plain::Int(1)])], Ok(Plain::FrozenSet(vec![Plain::Int(1)]))), + ("bytes", vec![Plain::Int(3)], Ok(b(b"\x00\x00\x00"))), + ("bytes", vec![Plain::List(vec![Plain::Int(65), Plain::Int(66)])], Ok(b(b"AB"))), + ("bytes", vec![s("é"), s("utf-8")], Ok(b(b"\xc3\xa9"))), + ("bytes", vec![Plain::List(vec![Plain::Int(300)])], Err(("ValueError", "bytes must be in range(0, 256)"))), + ("bytearray", vec![b(b"x")], Ok(Plain::ByteArray(b"x".to_vec()))), + ("reversed", vec![Plain::List(vec![Plain::Int(1), Plain::Int(2), Plain::Int(3)])], Ok(Plain::List(vec![Plain::Int(3), Plain::Int(2), Plain::Int(1)]))), + ("reversed", vec![s("ab")], Ok(Plain::List(vec![s("b"), s("a")]))), + ("reversed", vec![Plain::Range(0, 3, 1)], Ok(Plain::List(vec![Plain::Int(2), Plain::Int(1), Plain::Int(0)]))), + ("reversed", vec![Plain::Dict(vec![(s("a"), Plain::Int(1)), (s("b"), Plain::Int(2))])], Ok(Plain::List(vec![s("b"), s("a")]))), + ("reversed", vec![Plain::Int(5)], Err(("TypeError", "'int' object is not reversible"))), + ("min", vec![Plain::List(vec![Plain::Int(3), Plain::Int(1), Plain::Int(2)])], Ok(Plain::Int(1))), + ("min", vec![Plain::Int(3), Plain::Int(1)], Ok(Plain::Int(1))), + ("max", vec![s("abc")], Ok(s("c"))), + ("max", vec![Plain::List(vec![Plain::Tuple(vec![Plain::Int(1), s("b")]), Plain::Tuple(vec![Plain::Int(1), s("a")])])], Ok(Plain::Tuple(vec![Plain::Int(1), s("b")]))), + ("min", vec![Plain::List(vec![])], Err(("ValueError", "min() iterable argument is empty"))), + ("max", vec![Plain::List(vec![Plain::Int(1), s("a")])], Err(("TypeError", "'>' not supported between instances of 'str' and 'int'"))), + ("min", vec![Plain::List(vec![Plain::Float(2.5), Plain::Int(1), Plain::Int(-3)])], Ok(Plain::Int(-3))), + ("sum", vec![Plain::List(vec![Plain::Int(1), Plain::Int(2), Plain::Int(3)])], Ok(Plain::Int(6))), + ("sum", vec![Plain::List(vec![Plain::Float(1.5), Plain::Int(2)])], Ok(Plain::Float(3.5))), + ("sum", vec![Plain::List(vec![Plain::Int(1), Plain::Int(2)]), Plain::Int(10)], Ok(Plain::Int(13))), + ("sum", vec![Plain::List(vec![Plain::List(vec![Plain::Int(1)]), Plain::List(vec![Plain::Int(2)])]), Plain::List(vec![])], Ok(Plain::List(vec![Plain::Int(1), Plain::Int(2)]))), + ("sum", vec![Plain::List(vec![s("a")])], Err(("TypeError", "unsupported operand type(s) for +: 'int' and 'str'"))), + ("sum", vec![Plain::List(vec![Plain::Int(4611686018427387904), Plain::Int(4611686018427387904)])], Ok(big("9223372036854775808"))), + ("sum", vec![Plain::Range(0, 101, 1)], Ok(Plain::Int(5050))), + ("all", vec![Plain::List(vec![Plain::Int(1), s("a")])], Ok(Plain::Bool(true))), + ("all", vec![Plain::List(vec![Plain::Int(1), Plain::Int(0)])], Ok(Plain::Bool(false))), + ("any", vec![Plain::List(vec![Plain::Int(0), s(""), Plain::None])], Ok(Plain::Bool(false))), + ("any", vec![Plain::List(vec![Plain::Int(0), Plain::List(vec![Plain::Int(1)])])], Ok(Plain::Bool(true))), + ("all", vec![Plain::List(vec![])], Ok(Plain::Bool(true))), + ("any", vec![Plain::Int(5)], Err(("TypeError", "'int' object is not iterable"))), + ("divmod", vec![Plain::Int(7), Plain::Int(2)], Ok(Plain::Tuple(vec![Plain::Int(3), Plain::Int(1)]))), + ("divmod", vec![Plain::Int(-7), Plain::Int(2)], Ok(Plain::Tuple(vec![Plain::Int(-4), Plain::Int(1)]))), + ("divmod", vec![Plain::Float(7.5), Plain::Int(2)], Ok(Plain::Tuple(vec![Plain::Float(3.0), Plain::Float(1.5)]))), + ("divmod", vec![Plain::Int(1), Plain::Int(0)], Err(("ZeroDivisionError", "division by zero"))), + ("pow", vec![Plain::Int(2), Plain::Int(10)], Ok(Plain::Int(1024))), + ("pow", vec![Plain::Int(2), Plain::Int(-1)], Ok(Plain::Float(0.5))), + ("pow", vec![Plain::Int(3), Plain::Int(4), Plain::Int(5)], Ok(Plain::Int(1))), + ("pow", vec![Plain::Int(2), Plain::Int(100)], Ok(big("1267650600228229401496703205376"))), + ("sorted", vec![Plain::List(vec![Plain::Int(3), Plain::Int(1), Plain::Int(2)])], Ok(Plain::List(vec![Plain::Int(1), Plain::Int(2), Plain::Int(3)]))), + ("sorted", vec![s("bca")], Ok(Plain::List(vec![s("a"), s("b"), s("c")]))), + ("sorted", vec![Plain::List(vec![Plain::Tuple(vec![Plain::Int(2), s("a")]), Plain::Tuple(vec![Plain::Int(1), s("b")]), Plain::Tuple(vec![Plain::Int(1), s("a")])])], Ok(Plain::List(vec![Plain::Tuple(vec![Plain::Int(1), s("a")]), Plain::Tuple(vec![Plain::Int(1), s("b")]), Plain::Tuple(vec![Plain::Int(2), s("a")])]))), + ("sorted", vec![Plain::List(vec![Plain::Int(1), s("a")])], Err(("TypeError", "'<' not supported between instances of 'str' and 'int'"))), + ("sorted", vec![Plain::List(vec![Plain::Float(2.5), Plain::Int(1), Plain::Bool(true)])], Ok(Plain::List(vec![Plain::Int(1), Plain::Bool(true), Plain::Float(2.5)]))), + ("sorted", vec![Plain::Dict(vec![(Plain::Int(3), Plain::Int(0)), (Plain::Int(1), Plain::Int(0))])], Ok(Plain::List(vec![Plain::Int(1), Plain::Int(3)]))), + ("enumerate", vec![Plain::List(vec![s("a"), s("b")])], Ok(Plain::List(vec![Plain::Tuple(vec![Plain::Int(0), s("a")]), Plain::Tuple(vec![Plain::Int(1), s("b")])]))), + ("enumerate", vec![s("xy"), Plain::Int(5)], Ok(Plain::List(vec![Plain::Tuple(vec![Plain::Int(5), s("x")]), Plain::Tuple(vec![Plain::Int(6), s("y")])]))), + ("zip", vec![Plain::List(vec![Plain::Int(1), Plain::Int(2), Plain::Int(3)]), s("ab")], Ok(Plain::List(vec![Plain::Tuple(vec![Plain::Int(1), s("a")]), Plain::Tuple(vec![Plain::Int(2), s("b")])]))), + ("zip", vec![Plain::List(vec![Plain::Int(1)]), Plain::List(vec![Plain::Int(2)]), Plain::List(vec![Plain::Int(3)])], Ok(Plain::List(vec![Plain::Tuple(vec![Plain::Int(1), Plain::Int(2), Plain::Int(3)])]))), + ("zip", vec![], Ok(Plain::List(vec![]))), + ("enumerate", vec![Plain::Int(5)], Err(("TypeError", "'int' object is not iterable"))), + ("str.__getitem__", vec![s("abc"), Plain::Slice(Box::new([big("1180591620717411303424"), Plain::None, Plain::None]))], Ok(s(""))), + ("str.__getitem__", vec![s("abc"), Plain::Slice(Box::new([big("-1180591620717411303424"), Plain::None, Plain::Int(-1)]))], Ok(s(""))), + ("str.__getitem__", vec![s("abc"), Plain::Slice(Box::new([Plain::Bool(true), Plain::None, Plain::None]))], Ok(s("bc"))), + ("str.__getitem__", vec![s("abc"), big("1180591620717411303424")], Err(("IndexError", "cannot fit 'int' into an index-sized integer"))), + ("str.find", vec![s("abc"), s("c"), big("1180591620717411303424")], Ok(Plain::Int(-1))), + ("str.find", vec![s("abc"), s("a"), big("-1180591620717411303424")], Ok(Plain::Int(0))), + ("str.count", vec![s("abc"), s("a"), big("-1180591620717411303424"), big("1180591620717411303424")], Ok(Plain::Int(1))), + ("str.startswith", vec![s("abc"), s("a"), big("-1180591620717411303424")], Ok(Plain::Bool(true))), + ("str.index", vec![s("abc"), s("c"), big("1180591620717411303424")], Err(("ValueError", "substring not found"))), + ("bytes.find", vec![b(b"abc"), b(b"c"), big("1180591620717411303424")], Ok(Plain::Int(-1))), + ("bytes.__getitem__", vec![b(b"abc"), Plain::Slice(Box::new([big("-1180591620717411303424"), big("1180591620717411303424"), Plain::None]))], Ok(b(b"abc"))), + ("list.__getitem__", vec![Plain::List(vec![Plain::Int(1), Plain::Int(2), Plain::Int(3)]), Plain::Slice(Box::new([big("1180591620717411303424"), big("-1180591620717411303424"), Plain::Int(-1)]))], Ok(Plain::List(vec![Plain::Int(3), Plain::Int(2), Plain::Int(1)]))), + ("list.index", vec![Plain::List(vec![Plain::Int(1), Plain::Int(2), Plain::Int(3)]), Plain::Int(3), big("-1180591620717411303424"), big("1180591620717411303424")], Ok(Plain::Int(2))), + ("tuple.index", vec![Plain::Tuple(vec![Plain::Int(1), Plain::Int(2)]), Plain::Int(1), big("1180591620717411303424")], Err(("ValueError", "tuple.index(x): x not in tuple"))), + ("slice.indices", vec![Plain::Slice(Box::new([big("-1180591620717411303424"), big("1180591620717411303424"), Plain::Int(1)])), Plain::Int(3)], Ok(Plain::Tuple(vec![Plain::Int(0), Plain::Int(3), Plain::Int(1)]))), + ("str.find", vec![s("abc"), s("b"), Plain::Bool(true)], Ok(Plain::Int(1))), + ("str.center", vec![s("a"), big("1180591620717411303424")], Err(("OverflowError", "Python int too large to convert to C ssize_t"))), + ("str.zfill", vec![s("a"), big("-1180591620717411303424")], Err(("OverflowError", "Python int too large to convert to C ssize_t"))), + ("str.expandtabs", vec![s("a\u{9}b"), big("1180591620717411303424")], Err(("OverflowError", "Python int too large to convert to C int"))), + ("bytes.ljust", vec![b(b"a"), big("1180591620717411303424")], Err(("OverflowError", "Python int too large to convert to C ssize_t"))), + ("bytearray.upper", vec![Plain::ByteArray(b"abc".to_vec())], Ok(Plain::ByteArray(b"ABC".to_vec()))), + ("bytearray.copy", vec![Plain::ByteArray(b"ab".to_vec())], Ok(Plain::ByteArray(b"ab".to_vec()))), + ("bytearray.split", vec![Plain::ByteArray(b"a b".to_vec())], Ok(Plain::List(vec![Plain::ByteArray(b"a".to_vec()), Plain::ByteArray(b"b".to_vec())]))), + ("bytearray.partition", vec![Plain::ByteArray(b"a=b".to_vec()), b(b"=")], Ok(Plain::Tuple(vec![Plain::ByteArray(b"a".to_vec()), Plain::ByteArray(b"=".to_vec()), Plain::ByteArray(b"b".to_vec())]))), + ] + } + + #[test] + fn builtins_give_what_python_gives() { + let mut off = Vec::new(); + for (f, args, want) in cases() { + let want = want.map_err(|(c, m)| Raised { class: c.into(), message: m.into() }); + let got = call_pure(f, &args); + if got != want { off.push(format!("{f}{args:?}\n got {got:?}\n want {want:?}")); } + } + assert!(off.is_empty(), "{}", off.join("\n")); + } +} diff --git a/tests/snapshot.rs b/tests/snapshot.rs index bcbf2179..a762f1e2 100644 --- a/tests/snapshot.rs +++ b/tests/snapshot.rs @@ -2,10 +2,10 @@ mod test { use compiler::lexer::lex; - use compiler::parser::{Parser, SSAChunk}; - use compiler::vm::VM; - use compiler::vm::snapshot; - use compiler::vm::types::{Limits, SchedulerStatus, VmErr}; + use compiler::code::{Parser, SSAChunk}; + use compiler::embed::VM; + use compiler::embed as snapshot; + use compiler::value::{Limits, SchedulerStatus, VmErr}; #[derive(serde::Deserialize)] #[serde(deny_unknown_fields)] @@ -65,7 +65,7 @@ mod test { assert!(lex_errs.is_empty(), "lex error in {:?}", src); let (mut chunk, errs) = Parser::new(src, tokens.into_iter()).parse(); assert!(errs.is_empty(), "parse error in {:?}: {:?}", src, errs.iter().map(|e| &e.msg).collect::>()); - compiler::vm::optimizer::constant_fold(&mut chunk); + compiler::code::constant_fold(&mut chunk); compiler::util::leak(Box::new(chunk)) } diff --git a/tests/tables.rs b/tests/tables.rs index 3ba3fc9e..80a1373a 100644 --- a/tests/tables.rs +++ b/tests/tables.rs @@ -1,8 +1,8 @@ /* The tables the engine makes public for tools that generate rules from it, checked against what the engine does. */ mod test { use std::collections::HashSet; - use compiler::parser::{OpCode, BUILTIN_TYPES}; - use compiler::vm::{ALL_METHODS, exc_parents, lookup_method}; + use compiler::code::OpCode; + use compiler::types::{ALL_METHODS, BUILTIN_TYPES, CLASS_DUNDERS, NativeFnId, class_dunder, exc_parents, lookup_method}; #[test] fn every_opcode_is_listed_once_at_its_byte() { @@ -53,9 +53,9 @@ mod test { fn the_exception_tree_is_what_issubclass_answers() { let src = "print(issubclass(TimeoutError, OSError), issubclass(KeyError, LookupError), issubclass(ExceptionGroup, BaseExceptionGroup))"; let (tokens, _) = compiler::lexer::lex(src); - let (chunk, errors) = compiler::parser::Parser::new(src, tokens.into_iter()).parse(); + let (chunk, errors) = compiler::code::Parser::new(src, tokens.into_iter()).parse(); assert!(errors.is_empty()); - let mut vm = compiler::vm::VM::with_limits(&chunk, compiler::vm::Limits::sandbox()); + let mut vm = compiler::embed::VM::with_limits(&chunk, compiler::embed::Limits::sandbox()); vm.run().unwrap(); assert_eq!(vm.output_text(), "True True True\n"); for (child, parent) in [("TimeoutError", "OSError"), ("KeyError", "LookupError")] { assert!(exc_parents(child).contains(&parent)); } @@ -63,8 +63,9 @@ mod test { #[test] fn a_function_scope_and_parameters_are_those_the_vm_resolved() { - use compiler::parser::Parser; - use compiler::vm::{Kind, ParamKind, VM}; + use compiler::code::Parser; + use compiler::code::{Kind, ParamKind}; + use compiler::embed::VM; let src = "g = 1\ndef f(a, *r, k=2, **m):\n x = a\n def h():\n return x\n return g\n"; let (tokens, _) = compiler::lexer::lex(src); let (chunk, errors) = Parser::new(src, tokens.into_iter()).parse(); @@ -80,4 +81,80 @@ mod test { assert_eq!(kinds, [ParamKind::Normal, ParamKind::Star, ParamKind::KwOnly, ParamKind::DoubleStar]); assert!(vm.scope_of(&chunk).is_none()); } + + #[test] + fn every_method_is_pure_or_says_why_not() { + let off: Vec = ALL_METHODS.iter().filter(|m| m.pure == m.excluded().is_some() || m.pure && m.mutating).map(|m| format!("{}.{}", m.ty, m.name)).collect(); + assert!(off.is_empty(), "neither pure nor excluded with a reason: {}", off.join(" ")); + let off: Vec<&str> = NativeFnId::ALL.iter().filter(|f| f.pure() == f.excluded().is_some()).map(|f| f.name()).collect(); + assert!(off.is_empty(), "builtins neither pure nor excluded with a reason: {}", off.join(" ")); + } + + #[test] + fn a_class_body_binds_every_listed_dunder_and_no_other() { + let compiles = |name: &str| { + let src = format!("class A:\n def {name}(self): pass\n"); + let (tokens, _) = compiler::lexer::lex(&src); + compiler::code::Parser::new(&src, tokens.into_iter()).parse().1.is_empty() + }; + let mut seen = HashSet::new(); + for &name in CLASS_DUNDERS { + assert!(seen.insert(name), "{name} listed twice"); + assert!(class_dunder(name) && compiles(name), "{name}"); + } + for name in ["__set__", "__delete__", "__set_name__", "__slots__"] { assert!(CLASS_DUNDERS.contains(&name), "{name}"); } + for name in ["__del__", "__sizeof__", "__bytes__"] { assert!(!class_dunder(name) && !compiles(name), "{name}"); } + } + + /* The bindings of the slots of `body` named `name`, the slots no variable takes left out. */ + fn bound(p: &compiler::code::Program, body: &compiler::code::SSAChunk, name: &str) -> Vec { + use compiler::code::Binding; + let (_, b) = p.bindings_of(body).expect("a body of the program"); + let mut all: Vec = body.names.iter().enumerate().filter(|(_, n)| compiler::code::ssa_strip(n) == name).map(|(i, _)| b[i]).collect(); + all.dedup(); + if all.iter().any(|b| *b != Binding::Local) { all.retain(|b| *b != Binding::Local); } + all + } + + #[test] + fn module_and_class_bodies_bind_as_the_vm_resolves_them() { + use compiler::code::{Binding::*, BodyKind, Program, compile}; + let global = |b: &[compiler::code::Binding]| matches!(b, [Global(_)]); + // A module binds into its globals, a function reads them. + let (c, e) = compile("x = 1\ndef f(): return x\nprint(len([x]))\n"); + assert!(e.is_empty()); + let p = Program::new(&c); + assert_eq!(p.bindings_of(&c).map(|b| b.0), Some(BodyKind::Module)); + assert!(global(&bound(&p, &c, "x")) && global(&bound(&p, &c, "f"))); + assert_eq!(p.bindings_of(&c.functions[0].1).map(|b| b.0), Some(BodyKind::Function(0))); + assert_eq!(bound(&p, &c.functions[0].1, "x"), bound(&p, &c, "x")); + // A class body's names are its members, a method reads past them to the module. + let (c, _) = compile("y = 2\nclass A:\n a = y\n def m(self): return a\n"); + let p = Program::new(&c); + let a = &c.classes[0]; + assert_eq!(p.bindings_of(a).map(|b| b.0), Some(BodyKind::Class)); + assert_eq!((bound(&p, a, "a"), bound(&p, a, "m")), (vec![Member], vec![Member])); + assert_eq!(bound(&p, a, "y"), bound(&p, &c, "y")); + assert!(global(&bound(&p, &a.functions[0].1, "a"))); + // A class inside a function reads the function's cells and borrows its plain locals, its methods share the cells. + let (c, _) = compile("def f():\n c = 1\n p = 2\n def g(): return c\n class B:\n u = c\n v = p\n def m(self): return c\n return B\n"); + let p = Program::new(&c); + let (f, b) = (&c.functions[0].1, &c.functions[0].1.classes[0]); + assert_eq!((bound(&p, f, "c"), bound(&p, f, "p")), (vec![Cell], vec![Local])); + assert_eq!((bound(&p, b, "c"), bound(&p, b, "p"), bound(&p, b, "u")), (vec![Cell], vec![Lent], vec![Member])); + let (_, m) = p.scope_of(&b.functions[0].1).expect("m is a function"); + assert_eq!(m.freevars.iter().map(|v| v.2.as_str()).collect::>(), ["c"]); + // A class body's `global` writes the module, and a class inside a class reads none of the function's plain locals. + let (c, _) = compile("def f():\n w = 1\n class D:\n global q\n q = 1\n class I:\n t = w\n return D\n"); + let p = Program::new(&c); + let d = &c.functions[0].1.classes[0]; + assert!(global(&bound(&p, d, "q")) && global(&bound(&p, &d.classes[0], "w"))); + // A comprehension in a class body is a function its members are not visible to, past the iterable it is handed. + let (c, _) = compile("class E:\n xs = [1, 2]\n ys = [i * 2 for i in xs]\n zs = [xs for i in range(2)]\n"); + let p = Program::new(&c); + let e = &c.classes[0]; + assert_eq!((bound(&p, e, "xs"), bound(&p, e, "ys")), (vec![Member], vec![Member])); + assert_eq!(bound(&p, &e.functions[0].1, "i"), vec![Local]); + assert!(global(&bound(&p, &e.functions[1].1, "xs"))); + } } diff --git a/tests/unmetered.rs b/tests/unmetered.rs index 3d8f36c8..6fd6cdd7 100644 --- a/tests/unmetered.rs +++ b/tests/unmetered.rs @@ -1,6 +1,6 @@ use compiler::lexer::lex; -use compiler::parser::Parser; -use compiler::vm::{Limits, VM}; +use compiler::code::Parser; +use compiler::embed::{Limits, VM}; // Without the metering allocator nothing counts the memory limit, so a run refuses it and runs without one. #[test] diff --git a/tests/vm.rs b/tests/vm.rs index 491c0898..85314d71 100644 --- a/tests/vm.rs +++ b/tests/vm.rs @@ -2,9 +2,9 @@ mod test { use compiler::lexer::lex; - use compiler::parser::Parser; - use compiler::vm::VM; - use compiler::vm::types::{SchedulerStatus, VmErr}; + use compiler::code::Parser; + use compiler::embed::VM; + use compiler::value::{SchedulerStatus, VmErr}; #[derive(serde::Deserialize)] #[serde(deny_unknown_fields)] @@ -91,7 +91,7 @@ mod test { } let (mut chunk, _errors) = Parser::new(&case.src, tokens.into_iter()).parse(); // Run the same fold pass production does (exports.rs), so fold-path bugs surface here. - compiler::vm::optimizer::constant_fold(&mut chunk); + compiler::code::constant_fold(&mut chunk); let mut vm = VM::with_limits(&chunk, crate::common::corpus_limits()); vm.input_buffer = case.input.clone(); hook_sends(&mut vm, &case); @@ -167,7 +167,7 @@ mod test { } } // Match production and fold before running. - compiler::vm::optimizer::constant_fold(&mut chunk); + compiler::code::constant_fold(&mut chunk); let mut vm = VM::with_limits(&chunk, crate::common::corpus_limits()); vm.input_buffer = case.input.clone(); @@ -199,10 +199,10 @@ mod test { /* An error the host delivers into a parked call renders like one the call raised itself. */ mod host_errors { use compiler::lexer::lex; - use compiler::parser::{Parser, SSAChunk}; - use compiler::vm::VM; - use compiler::vm::snapshot; - use compiler::vm::types::{Limits, SchedulerStatus, VmErr}; + use compiler::code::{Parser, SSAChunk}; + use compiler::embed::VM; + use compiler::embed as snapshot; + use compiler::value::{Limits, SchedulerStatus, VmErr}; use crate::common::{fail, test_native, TestResolver}; @@ -212,7 +212,7 @@ mod test { let (tokens, _) = lex(src); let (mut chunk, errs) = Parser::with_resolver(src, tokens.into_iter(), Box::new(resolver)).parse(); assert!(errs.is_empty(), "parse errors on {src:?}: {:?}", errs.iter().map(|e| &e.msg).collect::>()); - compiler::vm::optimizer::constant_fold(&mut chunk); + compiler::code::constant_fold(&mut chunk); chunk } @@ -396,7 +396,7 @@ mod test { // Builtins that scan or copy pay the op budget for what they read, so looping one cannot outrun it. #[test] fn native_work_pays_the_budget() { - use compiler::vm::types::Limits; + use compiler::value::Limits; for src in [ "s = 'ab' * 5000\nfor i in range(1000):\n n = s.count('b')", "l = list(range(10000))\nfor i in range(1000):\n t = -1 in l",