Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ unicode-normalization = "0.1"
unicode-properties = "0.1"
unicode_names2 = "2.0"
memmap2 = "0.9"
memchr = "2.8"
libc = "0.2"
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
rustls-pki-types = "1.7"
Expand Down
8 changes: 4 additions & 4 deletions crates/weavepy-capi/src/abstract_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ fn attr_lookup(o: &Object, key: &str) -> Option<Object> {
// Walk the MRO and invoke descriptor protocol if the
// resolved attribute is a property, classmethod, or
// staticmethod. Mirror the VM's `LOAD_ATTR` dispatcher.
let raw = inst.class.lookup(key)?;
let raw = inst.cls().lookup(key)?;
match &raw {
Object::Property(p) => {
let getter = p.fget.clone();
Expand All @@ -177,7 +177,7 @@ fn attr_lookup(o: &Object, key: &str) -> Option<Object> {
}
Object::StaticMethod(inner) => Some((**inner).clone()),
Object::ClassMethod(inner) => {
let class = Object::Type(inst.class.clone());
let class = Object::Type(inst.cls());
Some(Object::BoundMethod(weavepy_vm::sync::Rc::new(
weavepy_vm::object::BoundMethod {
receiver: class,
Expand Down Expand Up @@ -495,7 +495,7 @@ fn install_runtime_error(err: RuntimeError) {
match err {
RuntimeError::PyException(pe) => {
let cls = match &pe.instance {
Object::Instance(inst) => Some(inst.class.clone()),
Object::Instance(inst) => Some(inst.cls()),
_ => None,
};
crate::errors::set_pending(cls, Object::from_str(pe.message()));
Expand Down Expand Up @@ -644,7 +644,7 @@ pub unsafe extern "C" fn PyObject_IsInstance(o: *mut PyObject, cls: *mut PyObjec
_ => return 0,
};
let actual = match &ob {
Object::Instance(inst) => Some(inst.class.clone()),
Object::Instance(inst) => Some(inst.cls()),
Object::Type(_) => Some(weavepy_vm::builtin_types::builtin_types().type_.clone()),
_ => weavepy_vm::builtin_types::builtin_types()
.by_name(type_name(&ob))
Expand Down
2 changes: 1 addition & 1 deletion crates/weavepy-capi/src/argparse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ pub unsafe extern "C" fn _WeavePy_Format_Set(ty: *mut PyObject, msg: *const c_ch
} else {
match unsafe { crate::object::clone_object(ty) } {
Object::Type(t) => Some(t),
Object::Instance(inst) => Some(inst.class.clone()),
Object::Instance(inst) => Some(inst.cls()),
_ => None,
}
};
Expand Down
4 changes: 2 additions & 2 deletions crates/weavepy-capi/src/datetime_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ fn is_class_named(o: *mut PyObject, name: &str) -> c_int {
}
match unsafe { crate::object::clone_object(o) } {
Object::Instance(inst) => {
for cls in inst.class.mro.borrow().iter() {
for cls in inst.cls().mro.borrow().iter() {
if cls.name == name {
return 1;
}
Expand All @@ -662,7 +662,7 @@ fn is_class_named_exact(o: *mut PyObject, name: &str) -> c_int {
}
match unsafe { crate::object::clone_object(o) } {
Object::Instance(inst) => {
if inst.class.name == name {
if inst.cls().name == name {
1
} else {
0
Expand Down
8 changes: 4 additions & 4 deletions crates/weavepy-capi/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ fn message_for(o: &Object) -> String {
}
}
}
format!("<{}>", inst.class.name)
format!("<{}>", inst.cls().name)
}
Object::None => String::new(),
_ => format!("{o:?}"),
Expand All @@ -122,7 +122,7 @@ pub fn set_pending_from_runtime(err: RuntimeError) {
match err {
RuntimeError::PyException(pe) => {
let cls = match &pe.instance {
Object::Instance(inst) => Some(inst.class.clone()),
Object::Instance(inst) => Some(inst.cls()),
_ => None,
};
set_pending(cls, Object::from_str(pe.message()));
Expand Down Expand Up @@ -554,7 +554,7 @@ pub unsafe extern "C" fn PyErr_GivenExceptionMatches(
}
let given_ty = match unsafe { crate::object::clone_object(given) } {
Object::Type(t) => t,
Object::Instance(inst) => inst.class.clone(),
Object::Instance(inst) => inst.cls(),
_ => return 0,
};
let exc_obj = unsafe { crate::object::clone_object(exc) };
Expand Down Expand Up @@ -684,7 +684,7 @@ fn type_object_for(p: *mut PyObject) -> Option<Rc<TypeObject>> {
}
match unsafe { crate::object::clone_object(p) } {
Object::Type(t) => Some(t),
Object::Instance(inst) => Some(inst.class.clone()),
Object::Instance(inst) => Some(inst.cls()),
_ => None,
}
}
2 changes: 1 addition & 1 deletion crates/weavepy-capi/src/genericalloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ pub unsafe extern "C" fn PyObject_HashNotImplemented(o: *mut PyObject) -> isize
}
let obj = unsafe { crate::object::clone_object(o) };
let name = match &obj {
Object::Instance(inst) => inst.class.name.clone(),
Object::Instance(inst) => inst.cls().name.clone(),
_ => "object".to_owned(),
};
crate::errors::set_type_error(format!("unhashable type: '{name}'"));
Expand Down
2 changes: 1 addition & 1 deletion crates/weavepy-capi/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ fn install_runtime_error(err: RuntimeError) {
match err {
RuntimeError::PyException(pe) => {
let cls = match &pe.instance {
Object::Instance(inst) => Some(inst.class.clone()),
Object::Instance(inst) => Some(inst.cls()),
_ => None,
};
crate::errors::set_pending(cls, Object::from_str(pe.message()));
Expand Down
2 changes: 1 addition & 1 deletion crates/weavepy-capi/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ pub fn type_for_object(o: &Object) -> *mut PyTypeObject {
O::Slice(_) => PySlice_Type.as_ptr(),
O::Type(t) => find_type_ptr(t).unwrap_or_else(|| PyType_Type.as_ptr()),
O::Instance(inst) => {
find_type_ptr(&inst.class).unwrap_or_else(|| PyBaseObject_Type.as_ptr())
find_type_ptr(&inst.cls()).unwrap_or_else(|| PyBaseObject_Type.as_ptr())
}
_ => PyBaseObject_Type.as_ptr(),
}
Expand Down
29 changes: 29 additions & 0 deletions crates/weavepy-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,35 @@ The following implementation-specific options are available:
";

fn main() -> ExitCode {
run_on_large_stack(main_dispatch)
}

/// WeavePy evaluates Python by recursive descent, so Python call depth
/// maps onto native (Rust) stack depth (see `crates/weavepy-vm/src/
/// recursion.rs`). Run the whole interpreter on a thread with a large
/// stack reserve so that `sys.setrecursionlimit` — enforced by the VM's
/// recursion guard (RFC 0037) — is what bounds recursion, rather than
/// the fixed OS main-thread stack (8 MiB on Linux/macOS). This makes the
/// behaviour uniform across platforms *and* build profiles: debug builds
/// have much larger per-activation stack frames than release, so without
/// this a default `setrecursionlimit(1000)` would overflow the native
/// stack in debug before the guard could fire. The reserve is committed
/// lazily by the OS, so it costs address space, not memory.
fn run_on_large_stack(entry: fn() -> ExitCode) -> ExitCode {
const STACK_BYTES: usize = 1024 * 1024 * 1024; // 1 GiB reserve
match std::thread::Builder::new()
.name("weavepy-main".to_owned())
.stack_size(STACK_BYTES)
.spawn(entry)
{
Ok(handle) => handle.join().unwrap_or(ExitCode::FAILURE),
// Extremely unlikely, but if the OS refuses the thread, fall
// back to running on the current (main) thread.
Err(_) => entry(),
}
}

fn main_dispatch() -> ExitCode {
init_tracing();

let raw: Vec<String> = env::args().collect();
Expand Down
5 changes: 5 additions & 0 deletions crates/weavepy-cli/src/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,11 @@ fn needs_continuation(source: &str) -> bool {
span.end.0 as usize >= source.len().saturating_sub(1)
}
Err(parser::ParseError::Lex(lexer::LexError::UnterminatedString { .. })) => true,
// An unterminated (possibly triple-quoted) f-string literal is the
// multi-line-continuation case too — the user is still typing it.
// (`FstringExpectingBrace`/`...OrSpec` are real errors, not these.)
Err(parser::ParseError::Lex(lexer::LexError::UnterminatedFstring { .. })) => true,
Err(parser::ParseError::Lex(lexer::LexError::UnterminatedTripleFstring { .. })) => true,
Err(parser::ParseError::Lex(lexer::LexError::UnexpectedEof { .. })) => true,
Err(_) => false,
}
Expand Down
38 changes: 38 additions & 0 deletions crates/weavepy-compiler/src/bytecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
//! - **Experimental** for the binary encoding; we explicitly do not
//! promise wire compatibility with CPython's `.pyc` format.

/// Flag bit OR-ed into the [`OpCode::BinaryOp`] argument to mark an
/// *augmented* assignment (`a += b`). The low byte still encodes the
/// [`BinOpKind`]; the VM strips this bit to recover the operator and,
/// when set, first tries the in-place dunder (`__iadd__`, …) before the
/// regular binary fallback. Kept above `0xFF` so `arg as u8` recovers
/// the operator kind unchanged.
pub const BINARY_OP_INPLACE_FLAG: u32 = 0x100;

/// Sub-operation tag for [`OpCode::BinaryOp`]. Mirrors CPython 3.11+'s
/// `_NB_*` enumeration.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
Expand Down Expand Up @@ -94,6 +102,26 @@ impl BinOpKind {
}
}

/// The in-place dunder name for this operator (`a += b` → `__iadd__`).
/// Used by the VM's augmented-assignment path.
pub fn inplace_dunder(self) -> &'static str {
match self {
Self::Add => "__iadd__",
Self::Sub => "__isub__",
Self::Mult => "__imul__",
Self::Div => "__itruediv__",
Self::FloorDiv => "__ifloordiv__",
Self::Mod => "__imod__",
Self::Pow => "__ipow__",
Self::LShift => "__ilshift__",
Self::RShift => "__irshift__",
Self::BitOr => "__ior__",
Self::BitXor => "__ixor__",
Self::BitAnd => "__iand__",
Self::MatMult => "__imatmul__",
}
}

/// The opcode argument that encodes this binary operator.
pub fn as_arg(self) -> u32 {
self as u32
Expand Down Expand Up @@ -431,6 +459,15 @@ pub enum OpCode {
/// `doctest`. In "exec" mode an expression statement uses
/// `PopTop` instead.
PrintExpr,

/// Clear the cell at `co_freevars[arg]` (CPython `DELETE_DEREF`).
/// Empties the cell's contents without touching the value stack;
/// raises `NameError` if the cell is already empty. Used for
/// `del NAME` where NAME is a cell or free variable.
///
/// Appended at the end of the enum so existing `#[repr(u8)]`
/// discriminants stay stable for any cached bytecode.
DeleteDeref,
}

impl OpCode {
Expand Down Expand Up @@ -520,6 +557,7 @@ impl OpCode {
OpCode::MatchKeys => "MATCH_KEYS",
OpCode::GetLen => "GET_LEN",
OpCode::PrintExpr => "PRINT_EXPR",
OpCode::DeleteDeref => "DELETE_DEREF",
}
}
}
Expand Down
40 changes: 37 additions & 3 deletions crates/weavepy-compiler/src/cpython_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ fn map_to_cpython(ins: Instruction, nlocals: u32) -> MappedOp {
O::DeleteName => (op::DELETE_NAME, ins.arg),
O::LoadDeref => (op::LOAD_DEREF, ins.arg + nlocals),
O::StoreDeref => (op::STORE_DEREF, ins.arg + nlocals),
O::DeleteDeref => (op::DELETE_DEREF, ins.arg + nlocals),
O::MakeCell => (op::MAKE_CELL, ins.arg + nlocals),
// 3.13 has no real LOAD_CLOSURE opcode; cells live in the fast
// array and are loaded with LOAD_FAST.
Expand Down Expand Up @@ -383,6 +384,11 @@ pub struct CpythonCode {
pub firstlineno: u32,
/// One [`Position`] per code unit.
pub positions: Vec<Position>,
/// Code-unit offset of each WeavePy instruction's *opcode* unit (i.e.
/// past any `EXTENDED_ARG` prefix), indexed by WeavePy instruction
/// index. Multiply by 2 for the `co_code` byte offset CPython's
/// `f_lasti`/`tb_lasti` expose. Length equals the instruction count.
pub inst_offsets: Vec<u32>,
}

const CO_FAST_LOCAL: u8 = 0x20;
Expand Down Expand Up @@ -480,14 +486,24 @@ pub fn encode(code: &CodeObject) -> CpythonCode {
// Emit code units + per-unit positions.
let mut co_code: Vec<u8> = Vec::with_capacity(starts[n] * 2);
let mut positions: Vec<Position> = Vec::with_capacity(starts[n]);
let mut inst_offsets: Vec<u32> = Vec::with_capacity(n);
let firstlineno = code.linetable.first().copied().unwrap_or(1);
for i in 0..n {
let line = code.linetable.get(i).copied().unwrap_or(firstlineno) as i32;
// PEP-657 columns, when the compiler tracked them for this
// instruction. `col`/`end_col` are byte offsets (`-1` = unknown);
// `end_lineno` is `0` when unknown (fall back to the start line).
let cs = code.coltable.get(i).copied().unwrap_or_default();
let end_lineno = if cs.end_lineno != 0 {
cs.end_lineno as i32
} else {
line
};
let pos = Position {
lineno: line,
end_lineno: line,
col: None,
end_col: None,
end_lineno,
col: (cs.col >= 0).then_some(cs.col as u32),
end_col: (cs.end_col >= 0).then_some(cs.end_col as u32),
};
let arg = args[i];
// EXTENDED_ARG units carry the high base-256 digits, MSB first.
Expand All @@ -497,6 +513,9 @@ pub fn encode(code: &CodeObject) -> CpythonCode {
co_code.push(byte);
positions.push(pos);
}
// The opcode unit lands here, past any EXTENDED_ARG prefix — this is
// the code-unit offset CPython's `f_lasti`/`tb_lasti` point at.
inst_offsets.push((co_code.len() / 2) as u32);
co_code.push(mapped[i].cp_op);
co_code.push((arg & 0xFF) as u8);
positions.push(pos);
Expand All @@ -517,6 +536,7 @@ pub fn encode(code: &CodeObject) -> CpythonCode {
stacksize: compute_stacksize(code),
firstlineno,
positions,
inst_offsets,
}
}

Expand Down Expand Up @@ -1002,6 +1022,7 @@ fn map_from_cpython(cp_op: u8, arg: u32, nlocals: u32) -> Option<(OpCode, u32)>
op::DELETE_NAME => (O::DeleteName, arg),
op::LOAD_DEREF => (O::LoadDeref, arg.saturating_sub(nlocals)),
op::STORE_DEREF => (O::StoreDeref, arg.saturating_sub(nlocals)),
op::DELETE_DEREF => (O::DeleteDeref, arg.saturating_sub(nlocals)),
op::MAKE_CELL => (O::MakeCell, arg.saturating_sub(nlocals)),
op::LOAD_ATTR => (O::LoadAttr, arg >> 1),
op::STORE_ATTR => (O::StoreAttr, arg),
Expand Down Expand Up @@ -1089,6 +1110,19 @@ impl CodeObject {
pub fn to_cpython(&self) -> CpythonCode {
encode(self)
}

/// Translate a WeavePy instruction index into the `co_code` byte offset
/// CPython's `f_lasti`/`tb_lasti` expose (2 bytes/code unit, opcode past
/// any `EXTENDED_ARG` prefix). Keeps `co_positions()` / `dis` anchoring
/// consistent across the cache- and extended-arg-inflated encoding.
#[must_use]
pub fn cpython_lasti(&self, weavepy_index: u32) -> u32 {
let cp = self.to_cpython();
cp.inst_offsets
.get(weavepy_index as usize)
.map(|&unit| unit * 2)
.unwrap_or(weavepy_index * 2)
}
}

#[cfg(test)]
Expand Down
Loading
Loading