Skip to content
Closed
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: 1 addition & 1 deletion pyrefly/lib/alt/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ impl Display for ConditionRedundantReason {
}
}

static MAX_TUPLE_LENGTH: usize = 256;
pub(crate) const MAX_TUPLE_LENGTH: usize = 256;

impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
fn synthesized_functional_class_type(&self, call: &ExprCall) -> Option<Type> {
Expand Down
41 changes: 41 additions & 0 deletions pyrefly/lib/alt/operators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use crate::alt::answers::LookupAnswer;
use crate::alt::answers_solver::AnswersSolver;
use crate::alt::call::CallStyle;
use crate::alt::callable::CallArg;
use crate::alt::expr::MAX_TUPLE_LENGTH;
use crate::alt::unwrap::HintRef;
use crate::binding::binding::KeyAnnotation;
use crate::config::error_kind::ErrorKind;
Expand Down Expand Up @@ -315,6 +316,42 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
}
}

fn try_tuple_repeat(&self, lhs: &Type, rhs: &Type) -> Option<Type> {
let repeat_count = |ty: &Type| match ty {
Type::Literal(box Literal {
value: Lit::Int(n), ..
}) => n.as_i64(),
Comment on lines +321 to +323
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try_tuple_repeat only recognizes Literal[int] as the repeat count. In Python, bool is a subtype of int and tuple * True/False is valid; right now those cases will fall through to the generic __mul__ typing and won't preserve arity. Consider extracting the count via Literal.value.as_index_i64() (covers both Int and Bool) instead of matching only Lit::Int.

Suggested change
Type::Literal(box Literal {
value: Lit::Int(n), ..
}) => n.as_i64(),
Type::Literal(box Literal { value, .. }) => value.as_index_i64(),

Copilot uses AI. Check for mistakes.
_ => None,
};
let (tuple, repeats) = match (lhs, rhs) {
(Type::Tuple(tuple), rhs) => (tuple, repeat_count(rhs)?),
(lhs, Type::Tuple(tuple)) => (tuple, repeat_count(lhs)?),
_ => return None,
};
if repeats <= 0 {
return Some(self.heap.mk_concrete_tuple(Vec::new()));
}
if repeats == 1 {
return Some(self.heap.mk_tuple(tuple.clone()));
}
let repeats = usize::try_from(repeats).ok()?;
match tuple {
Tuple::Concrete(elements) => {
let total_len = elements.len().checked_mul(repeats)?;
if total_len > MAX_TUPLE_LENGTH {
return None;
}
let mut repeated = Vec::with_capacity(total_len);
for _ in 0..repeats {
repeated.extend(elements.iter().cloned());
}
Some(self.heap.mk_concrete_tuple(repeated))
}
Tuple::Unbounded(element) => Some(self.heap.mk_unbounded_tuple((**element).clone())),
Tuple::Unpacked(_) => None,
}
}

pub fn binop_infer(
&self,
x: &ExprBinOp,
Expand Down Expand Up @@ -405,6 +442,10 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
&& let Type::Tuple(r) = rhs
{
self.tuple_concat(l, r)
} else if x.op == Operator::Mult
&& let Some(result) = self.try_tuple_repeat(lhs, rhs)
{
result
Comment on lines 444 to +448
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tuple-repeat special case is implemented for normal binary * in binop_types, but *= goes through augassign_infer and currently has no analogous handling (unlike +=, which special-cases tuples). This can make x *= 2 produce a less precise type than x = x * 2 for tuples; consider adding a Operator::Mult branch in augassign_infer that reuses try_tuple_repeat.

Copilot uses AI. Check for mistakes.
} else if matches!(
x.op,
Operator::Add
Expand Down
2 changes: 1 addition & 1 deletion pyrefly/lib/test/lsp/lsp_interaction/pytorch_benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ fn test_pytorch_error_propagation_latency() {
};
// Use all available cores for realistic benchmarking
let mut interaction =
LspInteraction::new_with_args(args, NoTelemetry, Some(ThreadCount::AllThreads));
LspInteraction::new_with_args(args, NoTelemetry, Some(ThreadCount::AllThreads), None);
interaction.set_root(pytorch_root.clone());

interaction
Expand Down
14 changes: 14 additions & 0 deletions pyrefly/lib/test/tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,20 @@ def test(x: tuple[int] | tuple[str]) -> None:
"#,
);

testcase!(
test_tuple_repeat,
r#"
from typing import assert_type, Literal

assert_type((42,) * 2, tuple[Literal[42], Literal[42]])
assert_type(2 * (42,), tuple[Literal[42], Literal[42]])
assert_type((1, "x") * 2, tuple[Literal[1], Literal["x"], Literal[1], Literal["x"]])
assert_type((1,) * 0, tuple[()])
assert_type((1,) * -1, tuple[()])
assert_type((1,) * 257, tuple[Literal[1], ...])
"#,
);

testcase!(
test_unpack_tuple_with_double_def,
r#"
Expand Down
Loading