Skip to content

Commit 496d3cf

Browse files
committed
added symbolic operations in type expressions
1 parent d370f76 commit 496d3cf

12 files changed

Lines changed: 866 additions & 12 deletions

File tree

crates/by_transforms/src/transforms/ast_driver.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ use super::{
3939
float_const, force_unwrap, generic_call, generics, identity_swap, implicit_typing, init_method,
4040
intersection, just_float, kw_subscript, literal_types, main_function, modifiers,
4141
mutable_defaults, none_chain, not_type, optional_type, overload, postfix_await, propagate,
42-
repeated_underscore, sentinel, some_ctor, super_keyword, top_star, tuple_index, type_is,
43-
typed_dict_literal, typed_lambda, typeof_keyword, unpack, use_site_variance,
42+
repeated_underscore, sentinel, some_ctor, super_keyword, symbolic_type_op, top_star,
43+
tuple_index, type_is, typed_dict_literal, typed_lambda, typeof_keyword, unpack,
44+
use_site_variance,
4445
};
4546
use crate::Config;
4647
use crate::type_info::TypeInfo;
@@ -84,6 +85,11 @@ pub(crate) struct PassContext {
8485
/// Lines to append AFTER the spliced body (e.g. modifiers' auto-
8586
/// generated `__all__ = [...]`). Driver emits each as its own line
8687
pub(crate) epilogue: Vec<String>,
88+
/// Source ranges of operations that `symbolic_type_op` resolved up front
89+
/// (e.g. `1 + 1` → `Literal[2]`). Type-aware passes skip these via
90+
/// [`walk_type_positions_skipping`](super::type_expr_walker::walk_type_positions_skipping)
91+
/// so they don't re-process an operation that no longer appears in the output
92+
pub(crate) claimed_type_op_ranges: Vec<TextRange>,
8793
}
8894

8995
/// A single AST-level rewrite pass.
@@ -286,6 +292,17 @@ pub(crate) fn run_against_source<'a>(
286292
text_edits: RefCell::new(vec![]),
287293
};
288294

295+
// resolve symbolic operations in type positions (`1 + 1` → `Literal[2]`)
296+
// up front, from the original parse where `typeof` operands are still
297+
// intact for ty to read. the pass replaces each operation node and must run
298+
// before `typeof` lowering so a `typeof` operand is consumed here
299+
let symbolic_folds =
300+
symbolic_type_op::collect_symbolic_folds(parsed_handle.suite(), &semantic_model);
301+
let symbolic_needs_literal_import = symbolic_folds.needs_literal_import;
302+
let symbolic_needs_any_import = symbolic_folds.needs_any_import;
303+
ctx.claimed_type_op_ranges = symbolic_folds.claimed_ranges();
304+
let symbolic_pass = symbolic_type_op::SymbolicTypeOp::new(symbolic_folds);
305+
289306
let tuple_index_inner = tuple_index::TupleIndex::new();
290307
let tuple_index_pass = VisitorPass {
291308
inner: &tuple_index_inner,
@@ -395,7 +412,12 @@ pub(crate) fn run_against_source<'a>(
395412
&decorator_keyword_pass,
396413
&unpack_pass,
397414
&typed_dict_literal_pass,
398-
// AST-mutation passes second (may zero node ranges)
415+
// AST-mutation passes second (may zero node ranges).
416+
// symbolic_type_op replaces whole operation nodes (consuming any
417+
// `typeof` operand) and reads original source ranges, so it must run
418+
// first among the mutation passes — before `typeof` and before any
419+
// pass that zeroes ranges
420+
&symbolic_pass,
399421
&coalesce_pass,
400422
&cast_pass,
401423
&typeof_pass,
@@ -463,6 +485,18 @@ pub(crate) fn run_against_source<'a>(
463485
ctx.required_imports
464486
.push("from ty_extensions import TypeOf".to_owned());
465487
}
488+
// symbolic folds that produced a `Literal[..]` need the import, unless the
489+
// source already binds `Literal`
490+
if symbolic_needs_literal_import && !literal_types::literal_already_imported(&semantic_model) {
491+
ctx.required_imports
492+
.push("from typing import Literal".to_owned());
493+
}
494+
// symbolic folds that produced `Any` (e.g. `dynamic + 1`) need the import,
495+
// unless the source already binds `Any`
496+
if symbolic_needs_any_import && !semantic_model.is_bound_globally("Any") {
497+
ctx.required_imports
498+
.push("from typing import Any".to_owned());
499+
}
466500
if sentinel_inner.ever_changed() {
467501
ctx.required_imports
468502
.push("from typing_extensions import Sentinel".to_owned());

crates/by_transforms/src/transforms/callable.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,12 @@ impl<'ast> ruff_python_ast::visitor::Visitor<'ast> for ValueCallableWalker<'_, '
518518
impl TypeAwarePass for CallableSyntaxPass<'_> {
519519
fn run(&self, stmts: &[Stmt], types: &dyn TypeInfo, ctx: &mut PassContext) {
520520
let mut inner = CallableSyntax::new(self.source).with_types(types);
521-
crate::transforms::type_expr_walker::walk_type_positions(stmts, Some(types), &mut inner);
521+
crate::transforms::type_expr_walker::walk_type_positions_skipping(
522+
stmts,
523+
Some(types),
524+
&ctx.claimed_type_op_ranges,
525+
&mut inner,
526+
);
522527
// also lower callable types appearing in value positions; duplicate
523528
// edits over type-position callables dedup in the splice
524529
{

crates/by_transforms/src/transforms/intersection.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use ruff_python_ast::{
1919
use ruff_text_size::{Ranged, TextRange};
2020

2121
use super::ast_driver::{PassContext, TypeAwarePass, render_expr};
22-
use super::type_expr_walker::{Recurse, TypeExprVisitor, TypePos, walk_type_positions};
22+
use super::type_expr_walker::{Recurse, TypeExprVisitor, TypePos, walk_type_positions_skipping};
2323
use crate::type_info::TypeInfo;
2424

2525
pub(crate) struct IntersectionType;
@@ -36,7 +36,7 @@ impl TypeAwarePass for IntersectionType {
3636
edits: Vec::new(),
3737
needs_import: false,
3838
};
39-
walk_type_positions(stmts, Some(types), &mut state);
39+
walk_type_positions_skipping(stmts, Some(types), &ctx.claimed_type_op_ranges, &mut state);
4040
ctx.text_edits.extend(state.edits);
4141
if state.needs_import {
4242
ctx.required_imports

crates/by_transforms/src/transforms/literal_types.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ use ruff_python_ast::{Expr, ExprSubscript, Operator, Stmt, UnaryOp};
1414
use ruff_text_size::{Ranged, TextRange, TextSize};
1515

1616
use crate::transforms::ast_driver::{PassContext, TypeAwarePass};
17-
use crate::transforms::type_expr_walker::{Recurse, TypeExprVisitor, TypePos, walk_type_positions};
17+
use crate::transforms::type_expr_walker::{
18+
Recurse, TypeExprVisitor, TypePos, walk_type_positions_skipping,
19+
};
1820
use crate::type_info::TypeInfo;
1921

2022
pub(crate) struct LiteralType<'src> {
@@ -378,7 +380,7 @@ impl<'src> LiteralTypePass<'src> {
378380
impl TypeAwarePass for LiteralTypePass<'_> {
379381
fn run(&self, stmts: &[Stmt], types: &dyn TypeInfo, ctx: &mut PassContext) {
380382
let mut inner = LiteralType::new(self.source, types);
381-
walk_type_positions(stmts, Some(types), &mut inner);
383+
walk_type_positions_skipping(stmts, Some(types), &ctx.claimed_type_op_ranges, &mut inner);
382384
if inner.needs_literal_import && !literal_already_imported(types) {
383385
ctx.required_imports
384386
.push("from typing import Literal".to_owned());

crates/by_transforms/src/transforms/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pub(crate) mod sentinel;
3939
pub(crate) mod some_ctor;
4040
pub(crate) mod source_util;
4141
pub(crate) mod super_keyword;
42+
pub(crate) mod symbolic_type_op;
4243
pub(crate) mod top_star;
4344
pub(crate) mod tuple_index;
4445
pub(crate) mod type_expr_walker;

0 commit comments

Comments
 (0)