Skip to content

Commit 62eda52

Browse files
authored
allow standalone dotted ops, parse .op as (. op) (#37583)
1 parent aa5e76a commit 62eda52

File tree

9 files changed

+110
-15
lines changed

9 files changed

+110
-15
lines changed

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ New language features
1616
* `` (U+A71B), `` (U+A71C) and `` (U+A71D) can now also be used as operator
1717
suffixes. They can be tab-completed from `\^uparrow`, `\^downarrow` and `\^!` in the REPL
1818
([#37542]).
19+
* Standalone "dotted" operators now get parsed as `Expr(:., :op)`, which gets lowered to
20+
`Base.BroadcastFunction(op)`. This means `.op` is functionally equivalent to
21+
`(x...) -> (op).(x...)`, which can be useful for passing the broadcasted version of an
22+
operator to higher-order functions, like for example `map(.*, A, B)` for an elementwise
23+
product of two arrays of arrays. ([#37583])
1924

2025
Language changes
2126
----------------

base/broadcast.jl

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ using .Base.Cartesian
1111
using .Base: Indices, OneTo, tail, to_shape, isoperator, promote_typejoin,
1212
_msk_end, unsafe_bitgetindex, bitcache_chunks, bitcache_size, dumpbitcache, unalias
1313
import .Base: copy, copyto!, axes
14-
export broadcast, broadcast!, BroadcastStyle, broadcast_axes, broadcastable, dotview, @__dot__, broadcast_preserving_zero_d
14+
export broadcast, broadcast!, BroadcastStyle, broadcast_axes, broadcastable, dotview, @__dot__, broadcast_preserving_zero_d, BroadcastFunction
1515

1616
## Computing the result's axes: deprecated name
1717
const broadcast_axes = axes
@@ -1196,7 +1196,7 @@ function __dot__(x::Expr)
11961196
Meta.isexpr(x.args[1], :call) # function or macro definition
11971197
Expr(x.head, x.args[1], dotargs[2])
11981198
elseif x.head === :(<:) || x.head === :(>:)
1199-
tmp = x.head === :(<:) ? :(.<:) : :(.>:)
1199+
tmp = x.head === :(<:) ? :.<: : :.>:
12001200
Expr(:call, tmp, dotargs...)
12011201
else
12021202
if x.head === :&& || x.head === :||
@@ -1264,4 +1264,44 @@ end
12641264
end
12651265
@inline broadcasted(::S, f, args...) where S<:BroadcastStyle = Broadcasted{S}(f, args)
12661266

1267+
"""
1268+
BroadcastFunction{F} <: Function
1269+
1270+
Represents the "dotted" version of an operator, which broadcasts the operator over its
1271+
arguments, so `BroadcastFunction(op)` is functionally equivalent to `(x...) -> (op).(x...)`.
1272+
1273+
Can be created by just passing an operator preceded by a dot to a higher-order function.
1274+
1275+
# Examples
1276+
```jldoctest
1277+
julia> a = [[1 3; 2 4], [5 7; 6 8]];
1278+
1279+
julia> b = [[9 11; 10 12], [13 15; 14 16]];
1280+
1281+
julia> map(.*, a, b)
1282+
2-element Vector{Matrix{Int64}}:
1283+
[9 33; 20 48]
1284+
[65 105; 84 128]
1285+
1286+
julia> Base.BroadcastFunction(+)(a, b) == a .+ b
1287+
true
1288+
```
1289+
1290+
!!! compat "Julia 1.6"
1291+
`BroadcastFunction` and the standalone `.op` syntax are available as of Julia 1.6.
1292+
"""
1293+
struct BroadcastFunction{F} <: Function
1294+
f::F
1295+
end
1296+
1297+
@inline (op::BroadcastFunction)(x...; kwargs...) = op.f.(x...; kwargs...)
1298+
1299+
function Base.show(io::IO, op::BroadcastFunction)
1300+
print(io, BroadcastFunction, '(')
1301+
show(io, op.f)
1302+
print(io, ')')
1303+
nothing
1304+
end
1305+
Base.show(io::IO, ::MIME"text/plain", op::BroadcastFunction) = show(io, op)
1306+
12671307
end # module

base/show.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1511,7 +1511,10 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In
15111511
unhandled = false
15121512
# dot (i.e. "x.y"), but not compact broadcast exps
15131513
if head === :(.) && (nargs != 2 || !is_expr(args[2], :tuple))
1514-
if nargs == 2 && is_quoted(args[2])
1514+
# standalone .op
1515+
if nargs == 1 && args[1] isa Symbol && isoperator(args[1])
1516+
print(io, "(.", args[1], ")")
1517+
elseif nargs == 2 && is_quoted(args[2])
15151518
item = args[1]
15161519
# field
15171520
field = unquoted(args[2])

src/julia-parser.scm

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1045,7 +1045,10 @@
10451045
(define (parse-unary-call s op un spc)
10461046
(let ((next (peek-token s)))
10471047
(cond ((or (closing-token? next) (newline? next) (eq? next '=))
1048-
op) ; return operator by itself, as in (+)
1048+
(if (dotop? op)
1049+
;; standalone dotted operators are parsed as (|.| op)
1050+
(list '|.| (undotop op))
1051+
op)) ; return operator by itself, as in (+)
10491052
((or (eqv? next #\{) ;; this case is +{T}(x::T) = ...
10501053
(and (not un) (eqv? next #\( )))
10511054
(ts:put-back! s op spc)

src/julia-syntax.scm

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1767,7 +1767,7 @@
17671767
(args (map dot-to-fuse (cdr kws+args)))
17681768
(make `(call (top ,(if (null? kws) 'broadcasted 'broadcasted_kwsyntax)) ,@kws ,f ,@args)))
17691769
(if top (cons 'fuse make) make)))
1770-
(if (and (pair? e) (eq? (car e) '|.|))
1770+
(if (and (length= e 3) (eq? (car e) '|.|))
17711771
(let ((f (cadr e)) (x (caddr e)))
17721772
(cond ((or (atom? x) (eq? (car x) 'quote) (eq? (car x) 'inert) (eq? (car x) '$))
17731773
`(call (top getproperty) ,f ,x))
@@ -1778,12 +1778,21 @@
17781778
(make-fuse f (cdr x))))
17791779
(else
17801780
(error (string "invalid syntax \"" (deparse e) "\"")))))
1781-
(if (and (pair? e) (eq? (car e) 'call) (dotop-named? (cadr e)))
1782-
(let ((f (undotop (cadr e))) (x (cddr e)))
1783-
(if (and (eq? (identifier-name f) '^) (length= x 2) (integer? (cadr x)))
1784-
(make-fuse '(top literal_pow)
1785-
(list f (car x) (expand-forms `(call (call (core apply_type) (top Val) ,(cadr x))))))
1786-
(make-fuse f x)))
1781+
(if (and (pair? e) (eq? (car e) 'call))
1782+
(begin
1783+
(define (make-fuse- f x)
1784+
(if (and (eq? (identifier-name f) '^) (length= x 2) (integer? (cadr x)))
1785+
(make-fuse '(top literal_pow)
1786+
(list f (car x) (expand-forms `(call (call (core apply_type) (top Val) ,(cadr x))))))
1787+
(make-fuse f x)))
1788+
(let ((f (cadr e)))
1789+
(cond ((dotop-named? f)
1790+
(make-fuse- (undotop f) (cddr e)))
1791+
;; (.+)(a, b) is parsed as (call (|.| +) a b), but we still want it to fuse
1792+
((and (length= f 2) (eq? (car f) '|.|))
1793+
(make-fuse- (cadr f) (cddr e)))
1794+
(else
1795+
e))))
17871796
e)))
17881797
(let ((e (dot-to-fuse rhs #t)) ; an expression '(fuse func args) if expr is a dot call
17891798
(lhs-view (ref-to-view lhs))) ; x[...] expressions on lhs turn in to view(x, ...) to update x in-place
@@ -1963,8 +1972,12 @@
19631972
(map expand-forms (cdr e))))))
19641973

19651974
'|.|
1966-
(lambda (e) ; e = (|.| f x)
1967-
(expand-fuse-broadcast '() e))
1975+
(lambda (e)
1976+
(if (length= e 2)
1977+
;; e = (|.| op)
1978+
`(call (top BroadcastFunction) ,(cadr e))
1979+
;; e = (|.| f x)
1980+
(expand-fuse-broadcast '() e)))
19681981

19691982
'.=
19701983
(lambda (e)
@@ -2164,6 +2177,9 @@
21642177
(let ((f (cadr e)))
21652178
(cond ((dotop-named? f)
21662179
(expand-fuse-broadcast '() `(|.| ,(undotop f) (tuple ,@(cddr e)))))
2180+
;; "(.op)(...)"
2181+
((and (length= f 2) (eq? (car f) '|.|))
2182+
(expand-fuse-broadcast '() `(|.| ,(cadr f) (tuple ,@(cddr e)))))
21672183
((eq? f 'ccall)
21682184
(if (not (length> e 4)) (error "too few arguments to ccall"))
21692185
(let* ((cconv (cadddr e))

src/toplevel.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ jl_value_t *jl_toplevel_eval_flex(jl_module_t *JL_NONNULL m, jl_value_t *e, int
630630

631631
jl_expr_t *ex = (jl_expr_t*)e;
632632

633-
if (ex->head == dot_sym) {
633+
if (ex->head == dot_sym && jl_expr_nargs(ex) != 1) {
634634
if (jl_expr_nargs(ex) != 2)
635635
jl_eval_errorf(m, "syntax: malformed \".\" expression");
636636
jl_value_t *lhs = jl_exprarg(ex, 0);

test/broadcast.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,3 +944,9 @@ p = rand(4,4); r = rand(2,4);
944944
p0 = copy(p)
945945
@views @. p[1:2, :] += r
946946
@test p[1:2, :] p0[1:2, :] + r
947+
948+
@test identity(.+) == Broadcast.BroadcastFunction(+)
949+
@test identity.(.*) == Broadcast.BroadcastFunction(*)
950+
@test map(.+, [[1,2], [3,4]], [5, 6]) == [[6,7], [9,10]]
951+
@test repr(.!) == "Base.Broadcast.BroadcastFunction(!)"
952+
@test eval(:(.+)) == Base.BroadcastFunction(+)

test/show.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,3 +2020,6 @@ end
20202020
@test Base.make_typealias(M37012.AStruct{1}) === nothing
20212021
@test isempty(Base.make_typealiases(M37012.AStruct{1})[1])
20222022
@test string(M37012.AStruct{1}) == "$(curmod_prefix)M37012.AStruct{1}"
2023+
2024+
@test sprint(show, :(./)) == ":((./))"
2025+
@test sprint(show, :((.|).(.&, b))) == ":((.|).((.&), b))"

test/syntax.jl

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -861,7 +861,7 @@ let f = function (x::T, y::S) where T<:S where S
861861
end
862862

863863
# issue #20541
864-
@test Meta.parse("[a .!b]") == Expr(:hcat, :a, Expr(:call, :(.!), :b))
864+
@test Meta.parse("[a .!b]") == Expr(:hcat, :a, Expr(:call, :.!, :b))
865865

866866
@test Meta.lower(Main, :(a{1} = b)) == Expr(:error, "invalid type parameter name \"1\"")
867867
@test Meta.lower(Main, :(a{2<:Any} = b)) == Expr(:error, "invalid type parameter name \"2\"")
@@ -2343,5 +2343,24 @@ end
23432343

23442344
@test :(a +ꜝ b) == Expr(:call, :+ꜝ, :a, :b)
23452345

2346+
function ncalls_in_lowered(ex, fname)
2347+
lowered_exprs = Meta.lower(Main, ex).args[1].code
2348+
return count(lowered_exprs) do ex
2349+
Meta.isexpr(ex, :call) && ex.args[1] == fname
2350+
end
2351+
end
2352+
2353+
@testset "standalone .op" begin
2354+
@test :(.+) == Expr(:., :+)
2355+
@test :(map(.-, a)) == Expr(:call, :map, Expr(:., :-), :a)
2356+
2357+
@test ncalls_in_lowered(:(.*), GlobalRef(Base, :BroadcastFunction)) == 1
2358+
@test ncalls_in_lowered(:((.^).(a, b)), GlobalRef(Base, :broadcasted)) == 1
2359+
@test ncalls_in_lowered(:((.^).(a, b)), GlobalRef(Base, :BroadcastFunction)) == 1
2360+
@test ncalls_in_lowered(:((.+)(a, b .- (.^)(c, 2))), GlobalRef(Base, :broadcasted)) == 3
2361+
@test ncalls_in_lowered(:((.+)(a, b .- (.^)(c, 2))), GlobalRef(Base, :materialize)) == 1
2362+
@test ncalls_in_lowered(:((.+)(a, b .- (.^)(c, 2))), GlobalRef(Base, :BroadcastFunction)) == 0
2363+
end
2364+
23462365
# issue #37656
23472366
@test :(if true 'a' else 1 end) == Expr(:if, true, quote 'a' end, quote 1 end)

0 commit comments

Comments
 (0)