mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
526 lines
21 KiB
Rust
526 lines
21 KiB
Rust
use super::{
|
|
PY_CF_ALLOW_INCOMPLETE_INPUT, PY_CF_ALLOW_TOP_LEVEL_AWAIT, PY_CF_DONT_IMPLY_DEDENT,
|
|
PY_CF_IGNORE_COOKIE, PY_CF_ONLY_AST, PY_CF_OPTIMIZED_AST, PY_CF_SOURCE_IS_UTF8,
|
|
PY_CF_TYPE_COMMENTS,
|
|
};
|
|
|
|
#[pymodule]
|
|
pub(crate) mod _ast {
|
|
use crate::{
|
|
AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
|
|
builtins::{PyStr, PyStrRef, PyTupleRef, PyType, PyTypeRef, PyUtf8Str, PyUtf8StrRef},
|
|
class::{PyClassImpl, StaticType},
|
|
common::wtf8::Wtf8,
|
|
function::{FuncArgs, KwArgs, PyMethodDef, PyMethodFlags},
|
|
stdlib::_ast::repr,
|
|
types::{Constructor, Initializer},
|
|
warn,
|
|
};
|
|
use indexmap::IndexMap;
|
|
#[pyattr]
|
|
#[pyclass(module = "_ast", name = "AST")]
|
|
#[derive(Debug, PyPayload)]
|
|
pub(crate) struct NodeAst;
|
|
|
|
#[pyclass(with(Constructor, Initializer), flags(BASETYPE, HAS_DICT))]
|
|
impl NodeAst {
|
|
#[extend_class]
|
|
fn extend_class(ctx: &Context, class: &'static Py<PyType>) {
|
|
// AST types are mutable (heap types, not IMMUTABLETYPE)
|
|
// Safety: called during type initialization before any concurrent access
|
|
unsafe {
|
|
let flags = &class.slots.flags as *const crate::types::PyTypeFlags
|
|
as *mut crate::types::PyTypeFlags;
|
|
(*flags).remove(crate::types::PyTypeFlags::IMMUTABLETYPE);
|
|
}
|
|
let empty_tuple = ctx.empty_tuple.clone();
|
|
class.set_str_attr("_fields", empty_tuple.clone(), ctx);
|
|
class.set_str_attr("_attributes", empty_tuple.clone(), ctx);
|
|
class.set_str_attr("__match_args__", empty_tuple.clone(), ctx);
|
|
|
|
const AST_REDUCE: PyMethodDef = PyMethodDef::new_const(
|
|
"__reduce__",
|
|
|zelf: PyObjectRef, vm: &VirtualMachine| -> PyResult<PyTupleRef> {
|
|
ast_reduce(zelf, vm)
|
|
},
|
|
PyMethodFlags::METHOD,
|
|
None,
|
|
);
|
|
const AST_REPLACE: PyMethodDef = PyMethodDef::new_const(
|
|
"__replace__",
|
|
|zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine| -> PyResult {
|
|
ast_replace(zelf, args, vm)
|
|
},
|
|
PyMethodFlags::METHOD,
|
|
None,
|
|
);
|
|
|
|
class.set_str_attr("__reduce__", AST_REDUCE.to_proper_method(class, ctx), ctx);
|
|
class.set_str_attr("__replace__", AST_REPLACE.to_proper_method(class, ctx), ctx);
|
|
class.slots.repr.store(Some(ast_repr));
|
|
}
|
|
|
|
#[pyattr]
|
|
fn _fields(ctx: &Context) -> PyTupleRef {
|
|
ctx.empty_tuple.clone()
|
|
}
|
|
|
|
#[pyattr]
|
|
fn _attributes(ctx: &Context) -> PyTupleRef {
|
|
ctx.empty_tuple.clone()
|
|
}
|
|
|
|
#[pyattr]
|
|
fn __match_args__(ctx: &Context) -> PyTupleRef {
|
|
ctx.empty_tuple.clone()
|
|
}
|
|
|
|
#[pymethod]
|
|
fn __reduce__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
|
|
ast_reduce(zelf, vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn __replace__(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
|
|
ast_replace(zelf, args, vm)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn ast_reduce(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
|
|
let dict = zelf.as_object().dict();
|
|
let cls = zelf.class();
|
|
let type_obj: PyObjectRef = cls.to_owned().into();
|
|
|
|
let Some(dict) = dict else {
|
|
return Ok(vm.ctx.new_tuple(vec![type_obj]));
|
|
};
|
|
|
|
let fields = cls.get_attr(vm.ctx.intern_str("_fields"));
|
|
if let Some(fields) = fields {
|
|
let fields: Vec<PyStrRef> = fields.try_to_value(vm)?;
|
|
let mut positional: Vec<PyObjectRef> = Vec::new();
|
|
for field in fields {
|
|
if dict.get_item_opt::<Wtf8>(field.as_wtf8(), vm)?.is_some() {
|
|
positional.push(vm.ctx.none());
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
let args: PyObjectRef = vm.ctx.new_tuple(positional).into();
|
|
let dict_obj: PyObjectRef = dict.into();
|
|
return Ok(vm.ctx.new_tuple(vec![type_obj, args, dict_obj]));
|
|
}
|
|
|
|
Ok(vm
|
|
.ctx
|
|
.new_tuple(vec![type_obj, vm.ctx.new_tuple(vec![]).into(), dict.into()]))
|
|
}
|
|
|
|
pub(crate) fn ast_replace(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
|
|
if !args.args.is_empty() {
|
|
return Err(vm.new_type_error("__replace__() takes no positional arguments"));
|
|
}
|
|
|
|
let cls = zelf.class();
|
|
let fields = cls.get_attr(vm.ctx.intern_str("_fields"));
|
|
let attributes = cls.get_attr(vm.ctx.intern_str("_attributes"));
|
|
let dict = zelf.as_object().dict();
|
|
|
|
let mut expecting: std::collections::HashSet<String> = std::collections::HashSet::new();
|
|
if let Some(fields) = fields.clone() {
|
|
let fields: Vec<PyUtf8StrRef> = fields.try_to_value(vm)?;
|
|
for field in fields {
|
|
expecting.insert(field.as_str().to_owned());
|
|
}
|
|
}
|
|
if let Some(attributes) = attributes.clone() {
|
|
let attributes: Vec<PyUtf8StrRef> = attributes.try_to_value(vm)?;
|
|
for attr in attributes {
|
|
expecting.insert(attr.as_str().to_owned());
|
|
}
|
|
}
|
|
|
|
for (key, _value) in &args.kwargs {
|
|
if !expecting.remove(key) {
|
|
return Err(vm.new_type_error(format!(
|
|
"{}.__replace__ got an unexpected keyword argument '{}'.",
|
|
cls.name(),
|
|
key
|
|
)));
|
|
}
|
|
}
|
|
|
|
if let Some(dict) = dict.as_ref() {
|
|
for (key, _value) in dict.items_vec() {
|
|
if let Ok(key) = key.downcast::<PyUtf8Str>() {
|
|
expecting.remove(key.as_str());
|
|
}
|
|
}
|
|
if let Some(attributes) = attributes.clone() {
|
|
let attributes: Vec<PyUtf8StrRef> = attributes.try_to_value(vm)?;
|
|
for attr in attributes {
|
|
expecting.remove(attr.as_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Discard optional fields (T | None).
|
|
if let Some(field_types) = cls.get_attr(vm.ctx.intern_str("_field_types"))
|
|
&& let Ok(field_types) = field_types.downcast::<crate::builtins::PyDict>()
|
|
{
|
|
for (key, value) in field_types.items_vec() {
|
|
let Ok(key) = key.downcast::<PyUtf8Str>() else {
|
|
continue;
|
|
};
|
|
if value.fast_isinstance(vm.ctx.types.union_type) {
|
|
expecting.remove(key.as_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
if !expecting.is_empty() {
|
|
let mut names: Vec<String> = expecting
|
|
.into_iter()
|
|
.map(|name| format!("{name:?}"))
|
|
.collect();
|
|
names.sort();
|
|
let missing = names.join(", ");
|
|
let count = names.len();
|
|
return Err(vm.new_type_error(format!(
|
|
"{}.__replace__ missing {} keyword argument{}: {}.",
|
|
cls.name(),
|
|
count,
|
|
if count == 1 { "" } else { "s" },
|
|
missing
|
|
)));
|
|
}
|
|
|
|
let payload = vm.ctx.new_dict();
|
|
if let Some(dict) = dict {
|
|
if let Some(fields) = fields.clone() {
|
|
let fields: Vec<PyStrRef> = fields.try_to_value(vm)?;
|
|
for field in fields {
|
|
if let Some(value) = dict.get_item_opt::<Wtf8>(field.as_wtf8(), vm)? {
|
|
payload.set_item(field.as_object(), value, vm)?;
|
|
}
|
|
}
|
|
}
|
|
if let Some(attributes) = attributes.clone() {
|
|
let attributes: Vec<PyStrRef> = attributes.try_to_value(vm)?;
|
|
for attr in attributes {
|
|
if let Some(value) = dict.get_item_opt::<Wtf8>(attr.as_wtf8(), vm)? {
|
|
payload.set_item(attr.as_object(), value, vm)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (key, value) in args.kwargs {
|
|
payload.set_item(vm.ctx.intern_str(key), value, vm)?;
|
|
}
|
|
|
|
let type_obj: PyObjectRef = cls.to_owned().into();
|
|
let kwargs = payload
|
|
.items_vec()
|
|
.into_iter()
|
|
.map(|(key, value)| {
|
|
let key = key
|
|
.downcast::<PyUtf8Str>()
|
|
.map_err(|_| vm.new_type_error("keywords must be strings"))?;
|
|
Ok((key.as_str().to_owned(), value))
|
|
})
|
|
.collect::<PyResult<IndexMap<String, PyObjectRef>>>()?;
|
|
let result = type_obj.call(FuncArgs::new(vec![], KwArgs::new(kwargs)), vm)?;
|
|
Ok(result)
|
|
}
|
|
|
|
pub(crate) fn ast_repr(zelf: &crate::PyObject, vm: &VirtualMachine) -> PyResult<PyRef<PyStr>> {
|
|
let repr = repr::repr_ast_node(vm, &zelf.to_owned(), 3)?;
|
|
Ok(vm.ctx.new_str(repr))
|
|
}
|
|
|
|
impl Constructor for NodeAst {
|
|
type Args = FuncArgs;
|
|
|
|
fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
|
|
if args.args.is_empty()
|
|
&& args.kwargs.is_empty()
|
|
&& let Some(instance) = cls.get_attr(vm.ctx.intern_str("_instance"))
|
|
{
|
|
return Ok(instance);
|
|
}
|
|
|
|
// AST nodes accept extra arguments (unlike object.__new__)
|
|
// This matches CPython's behavior where AST has its own tp_new
|
|
let dict = if cls
|
|
.slots
|
|
.flags
|
|
.contains(crate::types::PyTypeFlags::HAS_DICT)
|
|
{
|
|
Some(vm.ctx.new_dict())
|
|
} else {
|
|
None
|
|
};
|
|
let zelf = vm.ctx.new_base_object(cls, dict);
|
|
|
|
// type.__call__ does not invoke slot_init after slot_new
|
|
// for types with a custom slot_new, so we must call it here.
|
|
Self::slot_init(zelf.clone(), args, vm)?;
|
|
|
|
Ok(zelf)
|
|
}
|
|
|
|
fn py_new(_cls: &Py<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
|
|
unimplemented!("use slot_new")
|
|
}
|
|
}
|
|
|
|
impl Initializer for NodeAst {
|
|
type Args = FuncArgs;
|
|
|
|
fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
|
|
let fields = zelf
|
|
.class()
|
|
.get_attr(vm.ctx.intern_str("_fields"))
|
|
.ok_or_else(|| {
|
|
let module = zelf
|
|
.class()
|
|
.get_attr(vm.ctx.intern_str("__module__"))
|
|
.and_then(|obj| obj.try_to_value::<String>(vm).ok())
|
|
.unwrap_or_else(|| "ast".to_owned());
|
|
vm.new_attribute_error(format!(
|
|
"type object '{}.{}' has no attribute '_fields'",
|
|
module,
|
|
zelf.class().name()
|
|
))
|
|
})?;
|
|
let fields: Vec<PyUtf8StrRef> = fields.try_to_value(vm)?;
|
|
let n_args = args.args.len();
|
|
if n_args > fields.len() {
|
|
return Err(vm.new_type_error(format!(
|
|
"{} constructor takes at most {} positional argument{}",
|
|
zelf.class().name(),
|
|
fields.len(),
|
|
if fields.len() == 1 { "" } else { "s" },
|
|
)));
|
|
}
|
|
|
|
// Track which fields were set
|
|
let mut set_fields = std::collections::HashSet::new();
|
|
let mut attributes: Option<Vec<PyStrRef>> = None;
|
|
|
|
for (name, arg) in fields.iter().zip(args.args) {
|
|
zelf.set_attr(name, arg, vm)?;
|
|
set_fields.insert(name.as_str().to_owned());
|
|
}
|
|
for (key, value) in args.kwargs {
|
|
if let Some(pos) = fields.iter().position(|f| f.as_bytes() == key.as_bytes())
|
|
&& pos < n_args
|
|
{
|
|
return Err(vm.new_type_error(format!(
|
|
"{} got multiple values for argument '{}'",
|
|
zelf.class().name(),
|
|
key
|
|
)));
|
|
}
|
|
|
|
if fields
|
|
.iter()
|
|
.all(|field| field.as_bytes() != key.as_bytes())
|
|
{
|
|
let attrs = if let Some(attrs) = &attributes {
|
|
attrs
|
|
} else {
|
|
let attrs = zelf
|
|
.class()
|
|
.get_attr(vm.ctx.intern_str("_attributes"))
|
|
.and_then(|attr| attr.try_to_value::<Vec<PyStrRef>>(vm).ok())
|
|
.unwrap_or_default();
|
|
attributes = Some(attrs);
|
|
attributes.as_ref().unwrap()
|
|
};
|
|
if attrs.iter().all(|attr| attr.as_bytes() != key.as_bytes()) {
|
|
let message = vm.ctx.new_str(format!(
|
|
"{}.__init__ got an unexpected keyword argument '{}'. \
|
|
Support for arbitrary keyword arguments is deprecated and will be removed in Python 3.15.",
|
|
zelf.class().name(),
|
|
key
|
|
));
|
|
warn::warn(
|
|
message.into(),
|
|
Some(vm.ctx.exceptions.deprecation_warning.to_owned()),
|
|
1,
|
|
None,
|
|
vm,
|
|
)?;
|
|
}
|
|
}
|
|
|
|
set_fields.insert(key.clone());
|
|
zelf.set_attr(vm.ctx.intern_str(key), value, vm)?;
|
|
}
|
|
|
|
// Use _field_types to determine defaults for unset fields.
|
|
// Only built-in AST node classes have _field_types populated.
|
|
let field_types = zelf.class().get_attr(vm.ctx.intern_str("_field_types"));
|
|
if let Some(Ok(ft_dict)) =
|
|
field_types.map(|ft| ft.downcast::<crate::builtins::PyDict>())
|
|
{
|
|
let expr_ctx_type: PyObjectRef =
|
|
super::super::pyast::NodeExprContext::make_static_type().into();
|
|
|
|
for field in &fields {
|
|
if set_fields.contains(field.as_str()) {
|
|
continue;
|
|
}
|
|
if let Some(ftype) = ft_dict.get_item_opt::<Wtf8>(field.as_wtf8(), vm)? {
|
|
if ftype.fast_isinstance(vm.ctx.types.union_type) {
|
|
// Optional field (T | None) — no default
|
|
} else if ftype.fast_isinstance(vm.ctx.types.generic_alias_type) {
|
|
// List field (list[T]) — default to []
|
|
let empty_list: PyObjectRef = vm.ctx.new_list(vec![]).into();
|
|
zelf.set_attr(vm.ctx.intern_str(field.as_wtf8()), empty_list, vm)?;
|
|
} else if ftype.is(&expr_ctx_type) {
|
|
// expr_context — default to Load()
|
|
let load_type =
|
|
super::super::pyast::NodeExprContextLoad::make_static_type();
|
|
let load_instance = load_type
|
|
.get_attr(vm.ctx.intern_str("_instance"))
|
|
.unwrap_or_else(|| {
|
|
vm.ctx.new_base_object(load_type, Some(vm.ctx.new_dict()))
|
|
});
|
|
zelf.set_attr(vm.ctx.intern_str(field.as_wtf8()), load_instance, vm)?;
|
|
} else {
|
|
// Required field missing: emit DeprecationWarning.
|
|
let message = vm.ctx.new_str(format!(
|
|
"{}.__init__ missing 1 required positional argument: '{}'",
|
|
zelf.class().name(),
|
|
field.as_wtf8()
|
|
));
|
|
warn::warn(
|
|
message.into(),
|
|
Some(vm.ctx.exceptions.deprecation_warning.to_owned()),
|
|
1,
|
|
None,
|
|
vm,
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn init(_zelf: PyRef<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
|
|
unreachable!("slot_init is defined")
|
|
}
|
|
}
|
|
|
|
#[pyattr(name = "PyCF_SOURCE_IS_UTF8")]
|
|
use super::PY_CF_SOURCE_IS_UTF8;
|
|
|
|
#[pyattr(name = "PyCF_DONT_IMPLY_DEDENT")]
|
|
use super::PY_CF_DONT_IMPLY_DEDENT;
|
|
|
|
#[pyattr(name = "PyCF_ONLY_AST")]
|
|
use super::PY_CF_ONLY_AST;
|
|
|
|
#[pyattr(name = "PyCF_IGNORE_COOKIE")]
|
|
use super::PY_CF_IGNORE_COOKIE;
|
|
|
|
#[pyattr(name = "PyCF_TYPE_COMMENTS")]
|
|
use super::PY_CF_TYPE_COMMENTS;
|
|
|
|
#[pyattr(name = "PyCF_ALLOW_TOP_LEVEL_AWAIT")]
|
|
use super::PY_CF_ALLOW_TOP_LEVEL_AWAIT;
|
|
|
|
#[pyattr(name = "PyCF_ALLOW_INCOMPLETE_INPUT")]
|
|
use super::PY_CF_ALLOW_INCOMPLETE_INPUT;
|
|
|
|
#[pyattr(name = "PyCF_OPTIMIZED_AST")]
|
|
use super::PY_CF_OPTIMIZED_AST;
|
|
|
|
pub(crate) fn module_exec(
|
|
vm: &VirtualMachine,
|
|
module: &Py<crate::builtins::PyModule>,
|
|
) -> PyResult<()> {
|
|
__module_exec(vm, module);
|
|
super::super::pyast::extend_module_nodes(vm, module);
|
|
|
|
let ast_type = module
|
|
.get_attr("AST", vm)?
|
|
.downcast::<PyType>()
|
|
.map_err(|_| vm.new_type_error("AST is not a type"))?;
|
|
let ctx = &vm.ctx;
|
|
let empty_tuple = ctx.empty_tuple.clone();
|
|
ast_type.set_str_attr("_fields", empty_tuple.clone(), ctx);
|
|
ast_type.set_str_attr("_attributes", empty_tuple.clone(), ctx);
|
|
ast_type.set_str_attr("__match_args__", empty_tuple.clone(), ctx);
|
|
|
|
const AST_REDUCE: PyMethodDef = PyMethodDef::new_const(
|
|
"__reduce__",
|
|
|zelf: PyObjectRef, vm: &VirtualMachine| -> PyResult<PyTupleRef> {
|
|
ast_reduce(zelf, vm)
|
|
},
|
|
PyMethodFlags::METHOD,
|
|
None,
|
|
);
|
|
const AST_REPLACE: PyMethodDef = PyMethodDef::new_const(
|
|
"__replace__",
|
|
|zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine| -> PyResult {
|
|
ast_replace(zelf, args, vm)
|
|
},
|
|
PyMethodFlags::METHOD,
|
|
None,
|
|
);
|
|
let base_type = NodeAst::static_type();
|
|
ast_type.set_str_attr(
|
|
"__reduce__",
|
|
AST_REDUCE.to_proper_method(base_type, ctx),
|
|
ctx,
|
|
);
|
|
ast_type.set_str_attr(
|
|
"__replace__",
|
|
AST_REPLACE.to_proper_method(base_type, ctx),
|
|
ctx,
|
|
);
|
|
ast_type.slots.repr.store(Some(ast_repr));
|
|
|
|
const EXPR_DOC: &str = "expr = BoolOp(boolop op, expr* values)\n\
|
|
| NamedExpr(expr target, expr value)\n\
|
|
| BinOp(expr left, operator op, expr right)\n\
|
|
| UnaryOp(unaryop op, expr operand)\n\
|
|
| Lambda(arguments args, expr body)\n\
|
|
| IfExp(expr test, expr body, expr orelse)\n\
|
|
| Dict(expr?* keys, expr* values)\n\
|
|
| Set(expr* elts)\n\
|
|
| ListComp(expr elt, comprehension* generators)\n\
|
|
| SetComp(expr elt, comprehension* generators)\n\
|
|
| DictComp(expr key, expr value, comprehension* generators)\n\
|
|
| GeneratorExp(expr elt, comprehension* generators)\n\
|
|
| Await(expr value)\n\
|
|
| Yield(expr? value)\n\
|
|
| YieldFrom(expr value)\n\
|
|
| Compare(expr left, cmpop* ops, expr* comparators)\n\
|
|
| Call(expr func, expr* args, keyword* keywords)\n\
|
|
| FormattedValue(expr value, int conversion, expr? format_spec)\n\
|
|
| Interpolation(expr value, constant str, int conversion, expr? format_spec)\n\
|
|
| JoinedStr(expr* values)\n\
|
|
| TemplateStr(expr* values)\n\
|
|
| Constant(constant value, string? kind)\n\
|
|
| Attribute(expr value, identifier attr, expr_context ctx)\n\
|
|
| Subscript(expr value, expr slice, expr_context ctx)\n\
|
|
| Starred(expr value, expr_context ctx)\n\
|
|
| Name(identifier id, expr_context ctx)\n\
|
|
| List(expr* elts, expr_context ctx)\n\
|
|
| Tuple(expr* elts, expr_context ctx)\n\
|
|
| Slice(expr? lower, expr? upper, expr? step)";
|
|
let expr_type = super::super::pyast::NodeExpr::static_type();
|
|
expr_type.set_attr(
|
|
identifier!(vm.ctx, __doc__),
|
|
vm.ctx.new_str(EXPR_DOC).into(),
|
|
);
|
|
Ok(())
|
|
}
|
|
}
|