Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement for f.(args...) syntax #31553

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ New language features

* `inv(::Missing)` has now been added and returns `missing` ([#31408]).

* A new syntax `for f.(...)` is added to create a non-`materialize`d `Broadcasted` object
from a given "dot call" expression ([#19198]).

Multi-threading changes
-----------------------

Expand Down
4 changes: 4 additions & 0 deletions base/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,10 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int)
print(io, head, ' ')
show_list(io, args, ", ", indent)

elseif nargs == 1 && head == :fordot
print(io, "for ")
show_list(io, args, ", ", indent)

elseif head === :macrocall && nargs >= 2
# first show the line number argument as a comment
if isa(args[2], LineNumberNode) || is_expr(args[2], :line)
Expand Down
4 changes: 3 additions & 1 deletion doc/src/devdocs/ast.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ For example `(call f x)` corresponds to `Expr(:call, :f, :x)` in Julia.
| `f(x, y=1, z=2)` | `(call f x (kw y 1) (kw z 2))` |
| `f(x; y=1)` | `(call f (parameters (kw y 1)) x)` |
| `f(x...)` | `(call f (... x))` |
| `f.(x)` | `(. f (tuple x))` |
| `for f.(x)` | `(fordot (. f (tuple x)))` |

`do` syntax:

Expand All @@ -48,6 +50,7 @@ call. Finally, chains of comparisons have their own special expression structure
| Input | AST |
|:----------- |:------------------------- |
| `x+y` | `(call + x y)` |
| `x.+y` | `(call .+ x y)` |
| `a+b+c+d` | `(call + a b c d)` |
| `2x` | `(call * 2 x)` |
| `a&&b` | `(&& a b)` |
Expand All @@ -59,7 +62,6 @@ call. Finally, chains of comparisons have their own special expression structure
| `a==b` | `(call == a b)` |
| `1<i<=n` | `(comparison 1 < i <= n)` |
| `a.b` | `(. a (quote b))` |
| `a.(b)` | `(. a b)` |
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed a.(b) in "Operators" table (here) and mention it as f.(x) in "Calls" table since it seems the documentation does not match with the implementation:

> (julia-parse "a.(b)")
(|.| a (tuple b))


### Bracketed forms

Expand Down
14 changes: 14 additions & 0 deletions doc/src/manual/arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,20 @@ julia> string.(1:3, ". ", ["First", "Second", "Third"])
"3. Third"
```

Normal dot calls are evaluated (or _materialized_) immediately. To construct an
intermediate representation without invoking the computation, prepend `for` to
the a dot calls.

```jldoctest
julia> bc = for (1:3).^2;

julia> bc isa Broadcast.Broadcasted # it's not an `Array`
true

julia> sum(bc) # summation without allocating any arrays
14
```

## Implementation

The base array type in Julia is the abstract type [`AbstractArray{T,N}`](@ref). It is parameterized by
Expand Down
62 changes: 48 additions & 14 deletions src/julia-parser.scm
Original file line number Diff line number Diff line change
Expand Up @@ -1330,11 +1330,18 @@
((while) (begin0 (list 'while (parse-cond s) (parse-block s))
(expect-end s word)))
((for)
(let* ((ranges (parse-comma-separated-iters s))
(body (parse-block s)))
(expect-end s word)
`(for ,(if (length= ranges 1) (car ranges) (cons 'block ranges))
,body)))
(let ((r (parse-iteration-spec-or-dotcall s)))
(case (car r)
;; for loop
((for)
(let* ((ranges (parse-comma-separated-iters-continued s (list (cdr r))))
(body (parse-block s)))
(expect-end s word)
`(for ,(if (length= ranges 1) (car ranges) (cons 'block ranges))
,body)))
;; lazy dotcall
(else
`(fordot ,(cdr r))))))

((let)
(let ((binds (if (memv (peek-token s) '(#\newline #\;))
Expand Down Expand Up @@ -1622,6 +1629,12 @@

;; as above, but allows both "i=r" and "i in r"
(define (parse-iteration-spec s)
(let ((r (parse-iteration-spec-or-dotcall s)))
(case (car r)
((for) (cdr r))
(else (error "invalid iteration specification")))))

(define (parse-iteration-spec-or-dotcall s)
(let* ((outer? (if (eq? (peek-token s) 'outer)
(begin
(take-token s)
Expand All @@ -1643,19 +1656,40 @@
;; should be: (error "invalid iteration specification")
(parser-depwarn s (string "for " (deparse `(= ,lhs ,rhs)) " " t)
(string "for " (deparse `(= ,lhs ,rhs)) "; " t)))
(if outer?
(cons
'for
(if outer?
`(= (outer ,lhs) ,rhs)
`(= ,lhs ,rhs))))
`(= ,lhs ,rhs)))))
((and (eq? lhs ':) (closing-token? t))
':)
(else (error "invalid iteration specification")))))
'(for . :))

(else
(if (dotcall? lhs)
(cons 'fordot lhs)
(error "invalid expression after `for`"))))))

(define (dotcall? ex)
(and (pair? ex)
(case (car ex)
((call)
(equal? (substring (string (cadr ex)) 0 1) "."))
((|.|) (and (pair? (cdr ex))
(pair? (cddr ex))
(pair? (caddr ex))
(eq? (caaddr ex) 'tuple)))
(else #f))))

(define (parse-comma-separated-iters s)
(let loop ((ranges '()))
(let ((r (parse-iteration-spec s)))
(case (peek-token s)
((#\,) (take-token s) (loop (cons r ranges)))
(else (reverse! (cons r ranges)))))))
(parse-comma-separated-iters-continued s (list (parse-iteration-spec s))))

(define (parse-comma-separated-iters-continued s ranges)
(case (peek-token s)
((#\,)
(take-token s)
(parse-comma-separated-iters-continued s (cons (parse-iteration-spec s) ranges)))
(else
(reverse! ranges))))

(define (parse-space-separated-exprs s)
(with-space-sensitive
Expand Down
22 changes: 17 additions & 5 deletions src/julia-syntax.scm
Original file line number Diff line number Diff line change
Expand Up @@ -1642,10 +1642,7 @@
`(block ,@stmts ,nuref))
expr))

; lazily fuse nested calls to expr == f.(args...) into a single broadcast call,
; or a broadcast! call if lhs is non-null.
(define (expand-fuse-broadcast lhs rhs)
(define (fuse? e) (and (pair? e) (eq? (car e) 'fuse)))
(define (expand-lazy-broadcast rhs)
(define (dot-to-fuse e (top #f)) ; convert e == (. f (tuple args)) to (fuse f args)
(define (make-fuse f args) ; check for nested (fuse f args) exprs and combine
(define (split-kwargs args) ; return (cons keyword-args positional-args) extracted from args
Expand Down Expand Up @@ -1682,7 +1679,15 @@
(list '^ (car x) (expand-forms `(call (call (core apply_type) (top Val) ,(cadr x))))))
(make-fuse f x)))
e)))
(let ((e (dot-to-fuse rhs #t)) ; an expression '(fuse func args) if expr is a dot call
(dot-to-fuse rhs #t))

(define (fuse? e) (and (pair? e) (eq? (car e) 'fuse)))

; lazily fuse nested calls to expr == f.(args...) into a single broadcast call,
; or a broadcast! call if lhs is non-null.
(define (expand-fuse-broadcast lhs rhs)
(let ((e ; an expression '(fuse func args) if expr is a dot call
(expand-lazy-broadcast rhs))
(lhs-view (ref-to-view lhs))) ; x[...] expressions on lhs turn in to view(x, ...) to update x in-place
(if (fuse? e)
; expanded to a fuse op call
Expand Down Expand Up @@ -1839,6 +1844,13 @@
(lambda (e)
(expand-fuse-broadcast (cadr e) (caddr e)))

'fordot
(lambda (e)
(let ((x (expand-lazy-broadcast (cadr e))))
(if (fuse? x)
(expand-forms (cdr x))
(error "non-dot call after `for`"))))

'|<:|
(lambda (e) (expand-forms `(call |<:| ,@(cdr e))))
'|>:|
Expand Down
8 changes: 8 additions & 0 deletions test/broadcast.jl
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,14 @@ let x = [1, 3.2, 4.7],
@test atan.(α, y') == broadcast(atan, α, y')
end

@testset "for f.(args...) syntax (#19198, #31088)" begin
let bc = for (1:3).^2
@test bc isa Broadcast.Broadcasted
@test sum(bc) == 14
end
@test sum(for (1:3).^2) == 14
end

# issue 14725
let a = Number[2, 2.0, 4//2, 2+0im] / 2
@test eltype(a) == Number
Expand Down
63 changes: 63 additions & 0 deletions test/syntax.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1839,3 +1839,66 @@ end
# issue #31404
f31404(a, b; kws...) = (a, b, kws.data)
@test f31404(+, (Type{T} where T,); optimize=false) === (+, (Type,), (optimize=false,))

# lazy dot call
@test Meta.parse("for f.(x)") == Expr(:fordot, :(f.(x)))
@test Meta.parse("for f.(g(x))") == Expr(:fordot, :(f.(g(x))))
@test Meta.parse("for .+ x") == Expr(:fordot, :(.+ x))
@test Meta.parse("for .+ g(x)") == Expr(:fordot, :(.+ g(x)))

@test Meta.parse("""
for x in for f.(xs)
end
""") == Expr(:for,
Expr(:(=), :x, Expr(:fordot, :(f.(xs)))),
Expr(:block, LineNumberNode(2, :none)))

@test Meta.parse("""
for x in for f.(xs), y in for g.(ys)
end
""") == Expr(:for,
Expr(:block,
Expr(:(=), :x, Expr(:fordot, :(f.(xs)))),
Expr(:(=), :y, Expr(:fordot, :(g.(ys))))),
Expr(:block, LineNumberNode(2, :none)))

@testset for valid in [
"for f.(x)"
"for f.(g(x))"
"for .+ x"
"for .+ g(x)"
"for f.(args[1], args[2])"
"for f.(args...)"
]
@test Meta.lower(@__MODULE__, Meta.parse(valid)).head == :thunk
end

@testset for invalid in [
"for x"
"for f(x)"
"for f(g.(x))"
"for + x"
"for + f.(x))"
]
@test_throws ParseError Meta.parse(invalid)
end

@testset for code in [
"for f.(x)"
"for f.(g(x))"
"for (.+)(x)"
"for (.+)(g(x))"
]
@test string(Meta.parse(code)) == code
end

@testset for nondot in [
:(f(x))
:(f(g.(x)))
:(+f.(x))
1
quote end
]
@test Meta.lower(@__MODULE__, Expr(:fordot, nondot)) ==
Expr(:error, "non-dot call after `for`")
end