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
12 changes: 11 additions & 1 deletion crates/pyrefly_python/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,17 @@ impl Ast {
}

pub fn is_synthesized_empty_name(x: &ExprName) -> bool {
x.id.as_str().is_empty() && x.range.is_empty()
// The parser uses empty identifiers when recovering from syntax errors.
// Treat any empty identifier as synthesized, even if we still know the range, so
// downstream stages don't try to bind it.
x.id.as_str().is_empty()
}

pub fn is_synthesized_empty_identifier(x: &Identifier) -> bool {
// The parser uses empty identifiers when recovering from syntax errors.
// Treat any empty identifier as synthesized, even if we still know the range, so
// downstream stages don't try to bind it.
x.id.as_str().is_empty()
}

/// Calls a function on all of the names bound by this lvalue expression.
Expand Down
14 changes: 14 additions & 0 deletions pyrefly/lib/binding/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,12 @@ impl BindingTable {
idx
}

fn insert_if_missing(&mut self, key: Key, make_value: impl Fn() -> Binding) -> Idx<Key> {
let idx = self.types.0.insert(key);
self.types.1.insert_if_missing(idx, make_value);
idx
}

/// Record the binding of a value to a variable in an Anywhere binding (which
/// will take the phi of all values bound at different points). If necessary, we
/// insert the Anywhere.
Expand Down Expand Up @@ -658,6 +664,14 @@ impl<'a> BindingsBuilder<'a> {
self.table.insert_overwrite(key, value)
}

pub fn insert_binding_if_missing(
&mut self,
key: Key,
make_value: impl Fn() -> Binding,
) -> Idx<Key> {
self.table.insert_if_missing(key, make_value)
}

/// Insert a binding into the bindings table, given the `idx` of a key that we obtained previously.
pub fn insert_binding_idx<K: Keyed>(&mut self, idx: Idx<K>, value: K::Value) -> Idx<K>
where
Expand Down
10 changes: 4 additions & 6 deletions pyrefly/lib/binding/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,12 +407,10 @@ impl<'a> BindingsBuilder<'a> {
}
self.scopes.push(Scope::comprehension(range, is_generator));
}
// Incomplete nested comprehensions can have identical iterators
// for inner and outer loops. It is safe to overwrite it because it literally the same.
let iterable_value_idx = self.insert_binding_overwrite(
Key::Anon(comp.iter.range()),
Binding::IterableValue(None, comp.iter.clone(), IsAsync::new(comp.is_async)),
);
let iterable_value_idx = self
.insert_binding_if_missing(Key::Anon(comp.iter.range()), || {
Binding::IterableValue(None, comp.iter.clone(), IsAsync::new(comp.is_async))
});
self.scopes.add_lvalue_to_current_static(&comp.target);
// A comprehension target cannot be annotated, so it is safe to ignore the
// annotation (which is None) and just use a `Forward` here.
Expand Down
39 changes: 25 additions & 14 deletions pyrefly/lib/binding/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,14 +329,15 @@ impl<'a> BindingsBuilder<'a> {
// We ignore such names for first-usage-tracking purposes, since
// we are not going to analyze the code at all.
self.ensure_expr(illegal_target, &mut Usage::StaticTypeInformation);

// Make sure the RHS is properly bound, so that we can report errors there.
let mut user = self.declare_current_idx(Key::Anon(illegal_target.range()));
let mut usage = Usage::StaticTypeInformation;
if ensure_assigned && let Some(assigned) = &mut assigned {
self.ensure_expr(assigned, user.usage());
self.ensure_expr(assigned, &mut usage);
}
let binding = binding_of(make_assigned_value(assigned.as_deref(), None), None);
self.insert_binding_current(user, binding);
let key = Key::Anon(illegal_target.range());
self.insert_binding_if_missing(key, || {
binding_of(make_assigned_value(assigned.as_deref(), None), None)
});
}
}
}
Expand Down Expand Up @@ -417,18 +418,21 @@ impl<'a> BindingsBuilder<'a> {
make_binding: impl FnOnce(Option<&Expr>, Option<Idx<KeyAnnotation>>) -> Binding,
ensure_assigned: bool,
) {
let mut user = self.declare_current_idx(Key::Definition(ShortIdentifier::expr_name(name)));
if Ast::is_synthesized_empty_name(name) {
// Parser error recovery can synthesize empty identifiers. Skip creating a binding,
// but still analyze any assigned value so we surface downstream errors.
if ensure_assigned && let Some(assigned) = &mut assigned {
let mut usage = Usage::StaticTypeInformation;
self.ensure_expr(assigned, &mut usage);
}
return;
}
let identifier = ShortIdentifier::expr_name(name);
let mut user = self.declare_current_idx(Key::Definition(identifier));
if ensure_assigned && let Some(assigned) = &mut assigned {
self.ensure_expr(assigned, user.usage());
}
let ann = if !Ast::is_synthesized_empty_name(name) {
self.bind_current(&name.id, &user, FlowStyle::Other)
} else {
// Parser error recovery can synthesize walrus targets with empty identifiers.
// Pretend the name binding succeeded so we still analyze the RHS, but skip
// putting an entry into the flow-sensitive scope to avoid panics later.
None
};
let ann = self.bind_current(&name.id, &user, FlowStyle::Other);
let binding = make_binding(assigned.as_deref(), ann);
self.insert_binding_current(user, binding);
}
Expand All @@ -455,6 +459,13 @@ impl<'a> BindingsBuilder<'a> {
mut value: Box<Expr>,
direct_ann: Option<(&Expr, Idx<KeyAnnotation>)>,
) -> Option<Idx<KeyAnnotation>> {
if Ast::is_synthesized_empty_identifier(name) {
let range = value.range();
let mut user = self.declare_current_idx(Key::Anon(range));
self.ensure_expr(&mut value, user.usage());
self.insert_binding_current(user, Binding::Expr(None, *value));
return None;
}
let identifier = ShortIdentifier::new(name);
let mut current = self.declare_current_idx(Key::Definition(identifier));
let pinned_idx = self.idx_for_promise(Key::CompletedPartialType(identifier));
Expand Down
9 changes: 9 additions & 0 deletions pyrefly/lib/test/semantic_syntax_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ match p:
"#,
);

testcase!(
test_keyword_assignment_reports_parse_error,
r#"
async = 1 # E: Parse error: Expected `def`, `with` or `for` to follow `async`, found `=`
# From https://github.com/facebook/pyrefly/issues/2047
async = lambda: None # E: Parse error: Expected `def`, `with` or `for` to follow `async`, found `=`
"#,
);

testcase!(
test_duplicate_type_parameter_function,
r#"
Expand Down
Loading