Skip to content

Commit 224a52b

Browse files
committed
Bind $ receiver of method macros used as pipe stages
A method macro written as a pipe stage with a placeholder receiver (e.g. `xs |> $.foreach{...}`) failed at runtime with "name '_' is not defined". PlaceholderReplacer.visit_Subscript bailed out entirely on method-macro subscripts, skipping not just the macro's slice (correct -- that's its own scope) but also the receiver. With the receiver `$` unvisited, no stage lambda was built to bind it, so Python evaluated the bare placeholder before the macro handler could fire. Visit the receiver (node.value.value) while still deferring the slice, so the receiver placeholder gets wrapped into the pipe-stage lambda.
1 parent e2bf343 commit 224a52b

2 files changed

Lines changed: 49 additions & 0 deletions

File tree

pipescript/analysis/placeholders.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,13 @@ def visit_Subscript(self, node: ast.Subscript) -> None:
153153
isinstance(node.value, ast.Attribute)
154154
and node.value.attr in MacroTracer.dynamic_method_macros
155155
):
156+
# The macro's slice is its own scope (defer it), but the *receiver*
157+
# is part of the enclosing expression -- e.g. `$.foreach[...]` used
158+
# as a pipe stage (`xs |> $.foreach[...]`), where `$` is the piped
159+
# value. Visit the receiver so its placeholder gets wrapped into the
160+
# stage lambda; otherwise the receiver `$` evaluates as an unbound
161+
# name at runtime before the macro handler can fire.
162+
self.visit(node.value.value)
156163
return
157164
self.generic_visit(node)
158165

test/test_brace_block.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,48 @@ def test_nested_method_macro_scopes_inner_placeholder():
248248
assert ns["result"] == [(10, 0), (20, 0), (30, 1)], ns["result"]
249249

250250

251+
def test_method_macro_as_pipe_stage_with_placeholder_receiver():
252+
# `$.foreach[...]` as a pipe stage: the `$` receiver is the piped value and
253+
# must be bound by the stage lambda. Previously it evaluated as an unbound
254+
# name before the macro could fire ("name '_' is not defined").
255+
ns = pyc.exec(
256+
"out = []\n"
257+
"range(4) |> $.foreach[out.append($ ** 2)]\n"
258+
"result = out"
259+
)
260+
assert ns["result"] == [0, 1, 4, 9], ns["result"]
261+
262+
263+
def test_method_macro_brace_block_as_pipe_stage_with_placeholder_receiver():
264+
# the brace-block form of the above, with tuple-unpacking in the body
265+
ns = pyc.exec(
266+
"positions = {}\n"
267+
"[('a', 1), ('b', 2), ('a', 3)] |> enumerate |> $.foreach{\n"
268+
" i, kv = $\n"
269+
" positions.setdefault(kv[0], []).append((i, kv[1]))\n"
270+
"}\n"
271+
"result = positions"
272+
)
273+
assert ns["result"] == {"a": [(0, 1), (2, 3)], "b": [(1, 2)]}, ns["result"]
274+
275+
276+
def test_method_macro_pipe_stage_nested_in_block():
277+
# the user-reported case: a `... |> $.foreach{...}` pipeline nested inside an
278+
# enclosing `map{...}` block, whose own placeholder is distinct.
279+
ns = pyc.exec(
280+
"out = [['ab', 'cd'], ['ef']]\n"
281+
"result = out |> map{\n"
282+
" counts = {}\n"
283+
" $ |> enumerate |> $.foreach{\n"
284+
" i, s = $\n"
285+
" counts[i] = len(s)\n"
286+
" }\n"
287+
" counts\n"
288+
"} |> list"
289+
)
290+
assert ns["result"] == [{0: 2, 1: 2}, {0: 2}], ns["result"]
291+
292+
251293
def test_block_marker_emission_is_idempotent():
252294
# A host like ipyflow runs the syntax augmenter several times per cell; the
253295
# same block must always get the same marker id, or the rewriter's

0 commit comments

Comments
 (0)