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
9 changes: 9 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
.. default-role:: code

Unreleased
======================================================================

Supports Python 3.x – Python 3.y

New Features
------------------------------
* `setv` now supports chained assignment with `:chain`.

1.1.0 ("Business Hugs", released 2025-05-08)
======================================================================

Expand Down
6 changes: 6 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,12 @@ Assignment, mutation, and annotation
(print letter1 letter2 (hy.repr others))
; => a b ["c" "d" "e" "f" "g"]

Finally, as of Hy 1.2, you can precede an assignment pair with the keyword ``:chain`` to assign the same value multiple times to each of several targets. This construct compiles to a chained assignment in Python. ::

(setv :chain [x y z] 0)
(print x y z)
; => 0 0 0

See :hy:func:`let` to simulate more traditionally Lispy block-level scoping.

.. hy:macro:: (setx [target value])
Expand Down
48 changes: 26 additions & 22 deletions hy/core/result_macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,23 +481,27 @@ def compile_augassign_expression(compiler, expr, root, target, values):
# ------------------------------------------------


@pattern_macro("setv", [many(maybe_annotated(FORM) + FORM)])
@pattern_macro("setx", [times(1, 1, SYM + FORM)])
@pattern_macro("setv", [many(
(tag("chained", sym(":chain") + brackets(oneplus(FORM))) |
tag("maybe_annotated", maybe_annotated(FORM))) +
FORM)])
@pattern_macro("setx", [times(1, 1, tag("setx", SYM) + FORM)])
def compile_def_expression(compiler, expr, root, decls):
if not decls:
return asty.Constant(expr, value=None)

result = Result()
is_assignment_expr = root == "setx"
for decl in decls:
if is_assignment_expr:
ann = None
name, value = decl
else:
(name, ann), value = decl

for (ttag, target), value in decls:
ann = None
if ttag == "chained":
[target] = target
if ttag == "maybe_annotated":
target, ann = target
result += compile_assign(
compiler, ann, name, value, is_assignment_expr=is_assignment_expr
compiler, ann, target, value,
is_assignment_expr = ttag == "setx",
chained = ttag == "chained"
)
return result

Expand All @@ -521,7 +525,7 @@ def compile_basic_annotation(compiler, expr, root, target, ann):


def compile_assign(
compiler, ann, name, value, *, is_assignment_expr=False, let_scope=None
compiler, ann, target, value, *, is_assignment_expr=False, chained=False, let_scope=None
):
# Ensure that assignment expressions have a result and no annotation.
assert not is_assignment_expr or (value is not None and ann is None)
Expand All @@ -533,40 +537,40 @@ def compile_assign(
with let_scope or nullcontext():
result = compiler.compile(value)
if let_scope:
name = let_scope.add(name)

ld_name = compiler.compile(name)
target = let_scope.add(target)

if result.temp_variables and isinstance(name, Symbol):
result.rename(compiler, compiler._nonconst(name))
if result.temp_variables and isinstance(target, Symbol):
result.rename(compiler, compiler._nonconst(target))
if not is_assignment_expr:
# Throw away .expr to ensure that (setv ...) returns None.
result.expr = None
else:
st_name = compiler._storeize(name, ld_name)
st_targets = [
compiler._storeize(t, compiler.compile(t))
for t in (target if chained else [target])]

if ann is not None:
ann_result = compiler.compile(ann)
result = ann_result + result

target = dict(target = st_name)
target_kwarg = dict(target = st_targets[0])
if is_assignment_expr:
node = asty.NamedExpr
elif ann is not None:
node = lambda x, **kw: asty.AnnAssign(
x,
annotation=ann_result.force_expr,
simple=int(isinstance(name, Symbol)),
simple=int(isinstance(target, Symbol)),
**kw,
)
else:
node = asty.Assign
target = dict(targets = [st_name])
target_kwarg = dict(targets = st_targets)

result += node(
name if hasattr(name, "start_line") else result,
target if hasattr(target, "start_line") else result,
value=result.force_expr if not annotate_only else None,
**target
**target_kwarg
)

return result
Expand Down
36 changes: 36 additions & 0 deletions tests/native_tests/setv.hy
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,39 @@
(an (setv x.eggs "ham"))
(assert (not (hasattr x "eggs")))
(assert (= l [["eggs" "ham"]])))


(defn test-setv-chain []

(setv :chain [a b c] 3)
(assert (= a b c 3))

(setv x [])
(setv :chain [a b c] x)
(assert (is a b c x))

(setv v1 1 :chain [v2 v3] 2 v4 4 :chain [v5 v6 v7] 5)
(assert (= v1 1))
(assert (= v2 v3 2))
(assert (= v4 4))
(assert (= v5 v6 v7 5))

(setv :chain [[y #* z w] x [a b c d]] "abcd")
(assert (= [y z w] ["a" ["b" "c"] "d"]))
(assert (= x "abcd"))
(assert (= [a b c d] ["a" "b" "c" "d"]))

(setv l (* [0] 5))
(setv calls [])
(defn f [i]
(.append calls [i (list l)])
i)
(setv :chain
[(get l (f 1)) (get l (f 2)) (get l (f 3))]
(f 9))
(assert (= calls [
[9 [0 0 0 0 0]]
[1 [0 0 0 0 0]]
[2 [0 9 0 0 0]]
[3 [0 9 9 0 0]]]))
(assert (= l [0 9 9 9 0])))
3 changes: 3 additions & 0 deletions tests/resources/pydemo.hy
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ Call me Ishmael. Some years ago—never mind how long precisely—having little
(import itertools [cycle])
(setv mygenexpr (gfor x (cycle [1 2 3]) :if (!= x 2) x))

(setv [unpacked1 #* repacked unpacked2] "WXYZ")
(setv :chain [chained1 chained2 chained3] 77)

(setv attr-ref str.upper)
(setv subscript (get "hello" 2))
(setv myslice (cut "hello" 1 None 2))
Expand Down
3 changes: 3 additions & 0 deletions tests/test_hy2py.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ def assert_stuff(m):
assert type(m.mygenexpr) is type(x for x in [1, 2, 3])
assert list(itertools.islice(m.mygenexpr, 5)) == [1, 3, 1, 3, 1]

assert (m.unpacked1, m.repacked, m.unpacked2) == ("W", ["X", "Y"], "Z")
assert m.chained1 == m.chained2 == m.chained3 == 77

assert m.attr_ref is str.upper
assert m.subscript == "l"
assert m.myslice == "el"
Expand Down