Skip to content
Merged
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
6 changes: 4 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,10 @@ assignment, list, tuple, set and dict literals with unpacking, f-strings, slices
boolean operators, chained comparisons, keyword and starred arguments, `with`, `assert`,
`try`/`except`/`else`/`finally`, `await`, `yield`, `if`, `while` and `for` with their
`else` clauses, `break`, `continue`, `raise` with `from`, conditional expressions,
comprehensions, lambdas, nested functions and classes, `global`, `nonlocal`, `del` and
`match` with every pattern kind. `finally` is modelled on the normal path only and a
comprehensions, lambdas, nested functions and classes, `global`, `nonlocal`, `del`,
assignment expressions (`(y := f(x))`, laid out as an assignment before the statement),
`yield from`, starred assignment targets (`first, *rest = items`) and `match` with every
pattern kind. `finally` is modelled on the normal path only and a
`nonlocal` write stays inside the nested function. Methods of module-level classes are
analysed like functions.

Expand Down
9 changes: 7 additions & 2 deletions src/coretrace_python/cfg/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ def hoist(self, node: Any, pending: list[nodes.Statement]) -> Any:
return result
if isinstance(node, nodes.Comprehension):
return self.comprehension(node, pending)
if isinstance(node, nodes.NamedExpr):
pending.append(nodes.Assign(node.target, self.hoist(node.value, pending), node.span))
return node.target
if isinstance(node, nodes.Lambda):
defaults = tuple(
replace(p, default=self.hoist(p.default, pending)) if p.default is not None else p
Expand Down Expand Up @@ -481,7 +484,7 @@ def _may_hold_expressions(value: object) -> bool:


def _has_control_flow(node: object) -> bool:
if isinstance(node, nodes.Conditional | nodes.Comprehension):
if isinstance(node, nodes.Conditional | nodes.Comprehension | nodes.NamedExpr):
return True
if isinstance(node, nodes.Lambda):
return False
Expand All @@ -492,9 +495,11 @@ def _has_control_flow(node: object) -> bool:
return False


def _bound_names(target: nodes.Target) -> list[str]:
def _bound_names(target: nodes.Target | nodes.Starred) -> list[str]:
if isinstance(target, nodes.Name):
return [target.identifier]
if isinstance(target, nodes.Starred):
return _bound_names(target.value) # type: ignore[arg-type]
if isinstance(target, nodes.Tuple):
return [name for element in target.elements for name in _bound_names(element)] # type: ignore[arg-type]
return []
Expand Down
13 changes: 10 additions & 3 deletions src/coretrace_python/frontend/ast_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,11 @@ def expression(self, node: ast.expr) -> nodes.Expression:
yielded = self.expression(node.value) if node.value is not None else None
return nodes.Yield(yielded, span)
if isinstance(node, ast.YieldFrom):
self.fail(node, "yield from is not supported yet")
return nodes.Yield(self.expression(node.value), span, True)
if isinstance(node, ast.NamedExpr):
assert isinstance(node.target, ast.Name)
target = nodes.Name(node.target.id, self.span(node.target))
return nodes.NamedExpr(target, self.expression(node.value), span)
if isinstance(node, (ast.ListComp, ast.SetComp, ast.GeneratorExp)):
generators = tuple(self.generator(generator) for generator in node.generators)
return nodes.Comprehension(
Expand Down Expand Up @@ -178,10 +182,13 @@ def target(self, node: ast.expr) -> nodes.Target:
assert isinstance(target, (nodes.Attribute, nodes.Subscript))
return target
if isinstance(node, (ast.Tuple, ast.List)):
elements: list[nodes.Expression] = []
for element in node.elts:
if isinstance(element, ast.Starred):
self.fail(element, "starred assignment targets are not supported yet")
return nodes.Tuple(tuple(self.target(element) for element in node.elts), self.span(node))
elements.append(nodes.Starred(self.target(element.value), self.span(element)))
else:
elements.append(self.target(element))
return nodes.Tuple(tuple(elements), self.span(node))
self.fail(node, f"unsupported assignment target: {type(node).__name__}")

def generator(self, node: ast.comprehension) -> nodes.ComprehensionGenerator:
Expand Down
14 changes: 14 additions & 0 deletions src/coretrace_python/hir/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,21 @@ class Await:

@dataclass(frozen=True, slots=True)
class Yield:
"""``yield value``; with ``delegate``, ``yield from value``."""

value: Expression | None
span: SourceSpan
delegate: bool = False


@dataclass(frozen=True, slots=True)
class NamedExpr:
"""``(target := value)``: binds ``target`` in the enclosing function and evaluates to
the value. The CFG builder lays it out as an assignment before its statement."""

target: Name
value: Expression
span: SourceSpan


@dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -210,6 +223,7 @@ class Comprehension:
| Lambda
| Await
| Yield
| NamedExpr
| Comprehension
)

Expand Down
30 changes: 25 additions & 5 deletions src/coretrace_python/ir/lowering.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,29 @@ def store(self, target: nodes.Target, value: Value) -> None:
key = self.expression(target.key)
self.emit_effect(SetItem(None, target.span, object_value, key, value))
else:
for index, element in enumerate(target.elements):
position = self.emit(Constant(self.new_value(), element.span, index))
self.unpack(target, value)

def unpack(self, target: nodes.Tuple, value: Value) -> None:
"""Store each element of ``value`` into the targets; a starred target receives
the slice the others leave, and the targets after it index from the end."""

count = len(target.elements)
starred = next((i for i, e in enumerate(target.elements) if isinstance(e, nodes.Starred)), None)
for index, element in enumerate(target.elements):
if isinstance(element, nodes.Starred):
lower = self.emit(Constant(self.new_value(), element.span, index))
upper = None
if index < count - 1:
upper = self.emit(Constant(self.new_value(), element.span, index - count + 1))
bounds = self.emit(BuildSlice(self.new_value(), element.span, lower, upper, None))
item = self.emit(GetItem(self.new_value(), element.span, value, bounds))
element = element.value
else:
offset = index if starred is None or index < starred else index - count
position = self.emit(Constant(self.new_value(), element.span, offset))
item = self.emit(GetItem(self.new_value(), element.span, value, position))
assert isinstance(element, nodes.Name | nodes.Attribute | nodes.Subscript | nodes.Tuple)
self.store(element, item)
assert isinstance(element, nodes.Name | nodes.Attribute | nodes.Subscript | nodes.Tuple)
self.store(element, item)

def statement(self, node: nodes.Statement) -> None:
if isinstance(node, nodes.Assign):
Expand Down Expand Up @@ -466,9 +484,11 @@ def _reassigned_parameters(function: nodes.Function) -> frozenset[str]:

assigned: set[str] = set()

def names(target: nodes.Target | None) -> None:
def names(target: nodes.Target | nodes.Starred | None) -> None:
if isinstance(target, nodes.Name):
assigned.add(target.identifier)
elif isinstance(target, nodes.Starred):
names(target.value) # type: ignore[arg-type]
elif isinstance(target, nodes.Tuple):
for element in target.elements:
names(element) # type: ignore[arg-type]
Expand Down
9 changes: 9 additions & 0 deletions src/coretrace_python/semantic/scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,13 @@ def expression(self, node: nodes.Expression, scope: _ScopeBuilder) -> None:
elif isinstance(node, nodes.Yield):
if node.value is not None:
self.expression(node.value, scope)
elif isinstance(node, nodes.NamedExpr):
self.expression(node.value, scope)
# PEP 572: a walrus inside a comprehension binds in the containing scope.
owner = scope
while owner.kind is ScopeKind.COMPREHENSION and owner.parent is not None:
owner = owner.parent
owner.bind(node.target.identifier, BindingKind.LOCAL, node.target.span)
elif isinstance(node, nodes.Tuple | nodes.List):
for element in node.elements:
self.expression(element, scope)
Expand Down Expand Up @@ -419,6 +426,8 @@ def target(self, node: nodes.Target, scope: _ScopeBuilder) -> None:
scope.bind(node.identifier, BindingKind.LOCAL, node.span)
elif isinstance(node, nodes.Tuple):
for element in node.elements:
if isinstance(element, nodes.Starred):
element = element.value
assert isinstance(element, nodes.Name | nodes.Attribute | nodes.Subscript | nodes.Tuple)
self.target(element, scope)
else:
Expand Down
Loading
Loading