diff --git a/crates/pyrefly_python/src/sys_info.rs b/crates/pyrefly_python/src/sys_info.rs index 8499559b2b..0d9a30dcda 100644 --- a/crates/pyrefly_python/src/sys_info.rs +++ b/crates/pyrefly_python/src/sys_info.rs @@ -516,6 +516,52 @@ impl SysInfo { Some(self.evaluate(x)?.to_bool()) } + /// Return true/false only when evaluation depends on the configured Python environment. + pub fn evaluate_bool_with_sys_info(&self, x: &Expr) -> Option { + Self::depends_on_sys_info(x).then(|| self.evaluate_bool(x))? + } + + fn depends_on_sys_info(x: &Expr) -> bool { + match x { + Expr::Compare(x) => { + Self::depends_on_sys_info(&x.left) + || x.comparators.iter().any(Self::depends_on_sys_info) + } + Expr::Attribute(ExprAttribute { value, attr, .. }) => { + matches!( + &**value, + Expr::Name(name) + if (&name.id == "sys" && matches!(attr.as_str(), "platform" | "version_info")) + || (&name.id == "os" && attr.as_str() == "name") + || Self::is_type_checking_constant_name(attr.as_str()) + ) || Self::depends_on_sys_info(value) + } + Expr::Name(name) => Self::is_type_checking_constant_name(name.id()), + Expr::Call(x) => { + Self::depends_on_sys_info(&x.func) + || x.arguments.args.iter().any(Self::depends_on_sys_info) + || x.arguments + .keywords + .iter() + .any(|x| Self::depends_on_sys_info(&x.value)) + } + Expr::Subscript(x) => { + Self::depends_on_sys_info(&x.value) || Self::depends_on_sys_info(&x.slice) + } + Expr::Tuple(x) => x.elts.iter().any(Self::depends_on_sys_info), + Expr::List(x) => x.elts.iter().any(Self::depends_on_sys_info), + Expr::Set(x) => x.elts.iter().any(Self::depends_on_sys_info), + Expr::BoolOp(x) => x.values.iter().any(Self::depends_on_sys_info), + Expr::UnaryOp(x) => Self::depends_on_sys_info(&x.operand), + Expr::Slice(x) => { + x.lower.as_deref().is_some_and(Self::depends_on_sys_info) + || x.upper.as_deref().is_some_and(Self::depends_on_sys_info) + || x.step.as_deref().is_some_and(Self::depends_on_sys_info) + } + _ => false, + } + } + fn is_type_checking_constant_name(x: &str) -> bool { x == "TYPE_CHECKING" || x == "TYPE_CHECKING_WITH_PYREFLY" } diff --git a/pyrefly/lib/alt/expr.rs b/pyrefly/lib/alt/expr.rs index d52bd27898..ba6bc4835a 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -496,14 +496,24 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } Expr::If(x) => { let condition_type = self.expr_infer(&x.test, errors); - let body_type = self.expr_infer_impl(&x.body, hint, errors).into_ty(); - let orelse_type = self.expr_infer_impl(&x.orelse, hint, errors).into_ty(); self.check_dunder_bool_is_callable(&condition_type, x.range(), errors); self.check_redundant_condition(&condition_type, x.range(), errors); - match self.as_bool(&condition_type, x.test.range(), errors) { - Some(true) => body_type, - Some(false) => orelse_type, - None => self.union(body_type, orelse_type), + match self + .bindings() + .sys_info() + .evaluate_bool_with_sys_info(&x.test) + { + Some(true) => self.expr_infer_impl(&x.body, hint, errors).into_ty(), + Some(false) => self.expr_infer_impl(&x.orelse, hint, errors).into_ty(), + None => { + let body_type = self.expr_infer_impl(&x.body, hint, errors).into_ty(); + let orelse_type = self.expr_infer_impl(&x.orelse, hint, errors).into_ty(); + match self.as_bool(&condition_type, x.test.range(), errors) { + Some(true) => body_type, + Some(false) => orelse_type, + None => self.union(body_type, orelse_type), + } + } } } Expr::BoolOp(x) => self.boolop(&x.values, x.op, hint, errors), diff --git a/pyrefly/lib/binding/bindings.rs b/pyrefly/lib/binding/bindings.rs index 54b557dbd0..5dd902dffb 100644 --- a/pyrefly/lib/binding/bindings.rs +++ b/pyrefly/lib/binding/bindings.rs @@ -196,6 +196,7 @@ table! { #[derive(Clone, Debug)] struct BindingsInner { module_info: ModuleInfo, + sys_info: SysInfo, table: BindingTable, metadata: Arc, /// Multi-line ranges and ignore-all directives, computed from the AST @@ -351,6 +352,7 @@ impl Bindings { let module_info = Module::new(module_name, module_path, contents); Self(Arc::new(BindingsInner { module_info, + sys_info: SysInfo::default(), table: Default::default(), metadata: Arc::new(BindingsMetadata::new()), module_ranges: Arc::new(ModuleRanges { @@ -382,6 +384,10 @@ impl Bindings { &self.0.module_info } + pub fn sys_info(&self) -> &SysInfo { + &self.0.sys_info + } + pub fn metadata(&self) -> &Arc { &self.0.metadata } @@ -736,6 +742,7 @@ impl Bindings { let module_deletes = scope_trace.module_deletes().clone(); Self(Arc::new(BindingsInner { module_info, + sys_info: builder.sys_info, table: builder.table, metadata: Arc::new(builder.metadata), module_ranges, diff --git a/pyrefly/lib/binding/expr.rs b/pyrefly/lib/binding/expr.rs index 76c59cd7c3..d4387bd7dc 100644 --- a/pyrefly/lib/binding/expr.rs +++ b/pyrefly/lib/binding/expr.rs @@ -787,20 +787,49 @@ impl<'a> BindingsBuilder<'a> { // Process the test before forking so walrus-defined names are // in the base flow and visible to both branches. self.ensure_expr(&mut x.test, &mut Usage::non_pinning_value_from(usage)); + let static_test = self.sys_info.evaluate_bool_with_sys_info(&x.test); let narrow_ops = NarrowOps::from_expr(self, Some(&x.test)); self.start_fork_and_branch(x.range); - self.bind_narrow_ops(&narrow_ops, NarrowUseLocation::Span(x.body.range()), usage); - self.ensure_expr(&mut x.body, usage); - // Negate the narrow ops for the `orelse`, then merge the Flows. - // TODO(stroxler): We eventually want to drop all narrows but merge values. - self.next_branch(); - self.bind_narrow_ops( - &narrow_ops.negate(), - NarrowUseLocation::Span(x.range), - usage, - ); - self.ensure_expr(&mut x.orelse, usage); - self.finish_branch(); + match static_test { + Some(true) => { + self.bind_narrow_ops( + &narrow_ops, + NarrowUseLocation::Span(x.body.range()), + usage, + ); + self.ensure_expr(&mut x.body, usage); + self.finish_branch(); + } + Some(false) => { + self.abandon_branch(); + self.start_branch(); + self.bind_narrow_ops( + &narrow_ops.negate(), + NarrowUseLocation::Span(x.range), + usage, + ); + self.ensure_expr(&mut x.orelse, usage); + self.finish_branch(); + } + None => { + self.bind_narrow_ops( + &narrow_ops, + NarrowUseLocation::Span(x.body.range()), + usage, + ); + self.ensure_expr(&mut x.body, usage); + // Negate the narrow ops for the `orelse`, then merge the Flows. + // TODO(stroxler): We eventually want to drop all narrows but merge values. + self.next_branch(); + self.bind_narrow_ops( + &narrow_ops.negate(), + NarrowUseLocation::Span(x.range), + usage, + ); + self.ensure_expr(&mut x.orelse, usage); + self.finish_branch(); + } + } self.finish_exhaustive_fork(); } Expr::BoolOp(ExprBoolOp { diff --git a/pyrefly/lib/test/sys_info.rs b/pyrefly/lib/test/sys_info.rs index 0e011fb158..2f2beb7e05 100644 --- a/pyrefly/lib/test/sys_info.rs +++ b/pyrefly/lib/test/sys_info.rs @@ -192,6 +192,35 @@ assert_type(Y(), int) "#, ); +testcase!( + test_platform_ternary_linux, + TestEnv::new_with_platform(PythonPlatform::linux()), + r#" +import subprocess +import sys + +value: int = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0 +inverted: int = 0 if sys.platform != "win32" else subprocess.CREATE_NO_WINDOW + +def condition() -> bool: + return False + +subprocess.CREATE_NO_WINDOW if condition() else 0 # E: No attribute `CREATE_NO_WINDOW` in module `subprocess` +"#, +); + +testcase!( + test_platform_ternary_windows, + TestEnv::new_with_platform(PythonPlatform::windows()), + r#" +import subprocess +import sys + +value: int = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else "not windows" +inverted: int = "not windows" if sys.platform != "win32" else subprocess.CREATE_NO_WINDOW +"#, +); + testcase!( test_platform_membership, r#"