Skip to content

Commit

Permalink
support a[begin] for a[firstindex(a)] (JuliaLang#33946)
Browse files Browse the repository at this point in the history
* Revert "Back out `a[begin]` syntax"

This reverts commit e016f11.

* rm deprecation for a[begin...]

* fix parsing of begin in [...]

* fix printing of blocks inside indexing expressions
  • Loading branch information
stevengj authored and JeffBezanson committed Dec 13, 2019
1 parent f7b5faf commit 8b4232b
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 58 deletions.
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ New language features
* Function composition now supports multiple functions: `∘(f, g, h) = f ∘ g ∘ h`
and splatting `∘(fs...)` for composing an iterable collection of functions ([#33568]).

* `a[begin]` can now be used to address the first element of an integer-indexed collection `a`.
The index is computed by `firstindex(a)` ([#33946]).

Language changes
----------------

Expand Down
70 changes: 46 additions & 24 deletions base/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1115,7 +1115,7 @@ function show_unquoted_quote_expr(io::IO, @nospecialize(value), indent::Int, pre
end
else
if isa(value,Expr) && value.head === :block
show_block(io, "quote", value, indent, quote_level)
show_block(IOContext(io, beginsym=>false), "quote", value, indent, quote_level)
print(io, "end")
else
print(io, ":(")
Expand Down Expand Up @@ -1190,6 +1190,10 @@ function is_core_macro(arg, macro_name::AbstractString)
arg === GlobalRef(Core, Symbol(macro_name))
end

# symbol for IOContext flag signaling whether "begin" is treated
# as an ordinary symbol, which is true in indexing expressions.
const beginsym = gensym(:beginsym)

# TODO: implement interpolated strings
function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::Int = 0)
head, args, nargs = ex.head, ex.args, length(ex.args)
Expand Down Expand Up @@ -1324,7 +1328,7 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In
# other call-like expressions ("A[1,2]", "T{X,Y}", "f.(X,Y)")
elseif haskey(expr_calls, head) && nargs >= 1 # :ref/:curly/:calldecl/:(.)
funcargslike = head === :(.) ? args[2].args : args[2:end]
show_call(io, head, args[1], funcargslike, indent, quote_level)
show_call(head == :ref ? IOContext(io, beginsym=>true) : io, head, args[1], funcargslike, indent, quote_level)

# comprehensions
elseif head === :typed_comprehension && nargs == 2
Expand Down Expand Up @@ -1360,50 +1364,52 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In
# function calls need to transform the function from :call to :calldecl
# so that operators are printed correctly
elseif head === :function && nargs==2 && is_expr(args[1], :call)
show_block(io, head, Expr(:calldecl, args[1].args...), args[2], indent, quote_level)
show_block(IOContext(io, beginsym=>false), head, Expr(:calldecl, args[1].args...), args[2], indent, quote_level)
print(io, "end")

elseif (head === :function || head === :macro) && nargs == 1
print(io, head, ' ')
show_unquoted(io, args[1])
show_unquoted(IOContext(io, beginsym=>false), args[1])
print(io, " end")

elseif head === :do && nargs == 2
show_unquoted(io, args[1], indent, -1, quote_level)
iob = IOContext(io, beginsym=>false)
show_unquoted(iob, args[1], indent, -1, quote_level)
print(io, " do ")
show_list(io, args[2].args[1].args, ", ", 0, 0, quote_level)
show_list(iob, args[2].args[1].args, ", ", 0, 0, quote_level)
for stmt in args[2].args[2].args
print(io, '\n', " "^(indent + indent_width))
show_unquoted(io, stmt, indent + indent_width, -1, quote_level)
show_unquoted(iob, stmt, indent + indent_width, -1, quote_level)
end
print(io, '\n', " "^indent)
print(io, "end")

# block with argument
elseif head in (:for,:while,:function,:macro,:if,:elseif,:let) && nargs==2
if Meta.isexpr(args[2], :block)
show_block(io, head, args[1], args[2], indent, quote_level)
show_block(IOContext(io, beginsym=>false), head, args[1], args[2], indent, quote_level)
else
show_block(io, head, args[1], Expr(:block, args[2]), indent, quote_level)
show_block(IOContext(io, beginsym=>false), head, args[1], Expr(:block, args[2]), indent, quote_level)
end
print(io, "end")

elseif (head === :if || head === :elseif) && nargs == 3
show_block(io, head, args[1], args[2], indent, quote_level)
iob = IOContext(io, beginsym=>false)
show_block(iob, head, args[1], args[2], indent, quote_level)
if isa(args[3],Expr) && args[3].head === :elseif
show_unquoted(io, args[3], indent, prec, quote_level)
show_unquoted(iob, args[3], indent, prec, quote_level)
else
show_block(io, "else", args[3], indent, quote_level)
show_block(iob, "else", args[3], indent, quote_level)
print(io, "end")
end

elseif head === :module && nargs==3 && isa(args[1],Bool)
show_block(io, args[1] ? :module : :baremodule, args[2], args[3], indent, quote_level)
show_block(IOContext(io, beginsym=>false), args[1] ? :module : :baremodule, args[2], args[3], indent, quote_level)
print(io, "end")

# type declaration
elseif head === :struct && nargs==3
show_block(io, args[1] ? Symbol("mutable struct") : Symbol("struct"), args[2], args[3], indent, quote_level)
show_block(IOContext(io, beginsym=>false), args[1] ? Symbol("mutable struct") : Symbol("struct"), args[2], args[3], indent, quote_level)
print(io, "end")

elseif head === :primitive && nargs == 2
Expand All @@ -1413,7 +1419,7 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In

elseif head === :abstract && nargs == 1
print(io, "abstract type ")
show_list(io, args, ' ', indent, 0, quote_level)
show_list(IOContext(io, beginsym=>false), args, ' ', indent, 0, quote_level)
print(io, " end")

# empty return (i.e. "function f() return end")
Expand Down Expand Up @@ -1515,31 +1521,47 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In
show_linenumber(io, args...)

elseif head === :try && 3 <= nargs <= 4
show_block(io, "try", args[1], indent, quote_level)
iob = IOContext(io, beginsym=>false)
show_block(iob, "try", args[1], indent, quote_level)
if is_expr(args[3], :block)
show_block(io, "catch", args[2] === false ? Any[] : args[2], args[3], indent, quote_level)
show_block(iob, "catch", args[2] === false ? Any[] : args[2], args[3], indent, quote_level)
end
if nargs >= 4 && is_expr(args[4], :block)
show_block(io, "finally", Any[], args[4], indent, quote_level)
show_block(iob, "finally", Any[], args[4], indent, quote_level)
end
print(io, "end")

elseif head === :block
show_block(io, "begin", ex, indent, quote_level)
print(io, "end")
# print as (...; ...; ...;) inside indexing expression
if get(io, beginsym, false)
print(io, '(')
ind = indent + indent_width
for i = 1:length(ex.args)
i > 1 && print(io, ";\n", ' '^ind)
show_unquoted(io, ex.args[i], ind, -1, quote_level)
end
if length(ex.args) < 2
print(isempty(ex.args) ? "nothing;)" : ";)")
else
print(io, ')')
end
else
show_block(io, "begin", ex, indent, quote_level)
print(io, "end")
end

elseif head === :quote && nargs == 1 && isa(args[1], Symbol)
show_unquoted_quote_expr(io, args[1]::Symbol, indent, 0, quote_level+1)
show_unquoted_quote_expr(IOContext(io, beginsym=>false), args[1]::Symbol, indent, 0, quote_level+1)
elseif head === :quote && nargs == 1 && Meta.isexpr(args[1], :block)
show_block(io, "quote", Expr(:quote, args[1].args...), indent,
show_block(IOContext(io, beginsym=>false), "quote", Expr(:quote, args[1].args...), indent,
quote_level+1)
print(io, "end")
elseif head === :quote && nargs == 1
print(io, ":(")
show_unquoted(io, args[1], indent+2, 0, quote_level+1)
show_unquoted(IOContext(io, beginsym=>false), args[1], indent+2, 0, quote_level+1)
print(io, ")")
elseif head === :quote
show_block(io, "quote", ex, indent, quote_level+1)
show_block(IOContext(io, beginsym=>false), "quote", ex, indent, quote_level+1)
print(io, "end")

elseif head === :gotoifnot && nargs == 2 && isa(args[2], Int)
Expand Down
4 changes: 2 additions & 2 deletions doc/src/manual/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -838,8 +838,8 @@ the results (see [Pre-allocating outputs](@ref)). A convenient syntax for this i
is equivalent to `broadcast!(identity, X, ...)` except that, as above, the `broadcast!` loop is
fused with any nested "dot" calls. For example, `X .= sin.(Y)` is equivalent to `broadcast!(sin, X, Y)`,
overwriting `X` with `sin.(Y)` in-place. If the left-hand side is an array-indexing expression,
e.g. `X[2:end] .= sin.(Y)`, then it translates to `broadcast!` on a `view`, e.g.
`broadcast!(sin, view(X, 2:lastindex(X)), Y)`,
e.g. `X[begin+1:end] .= sin.(Y)`, then it translates to `broadcast!` on a `view`, e.g.
`broadcast!(sin, view(X, firstindex(X)+1:lastindex(X)), Y)`,
so that the left-hand side is updated in-place.

Since adding dots to many operations and function calls in an expression
Expand Down
8 changes: 4 additions & 4 deletions doc/src/manual/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,8 @@ julia> collect(Iterators.reverse(Squares(4)))
|:-------------------- |:-------------------------------- |
| `getindex(X, i)` | `X[i]`, indexed element access |
| `setindex!(X, v, i)` | `X[i] = v`, indexed assignment |
| `firstindex(X)` | The first index |
| `lastindex(X)` | The last index, used in `X[end]` |
| `firstindex(X)` | The first index, used in `X[begin]` |
| `lastindex(X)` | The last index, used in `X[end]` |

For the `Squares` iterable above, we can easily compute the `i`th element of the sequence by squaring
it. We can expose this as an indexing expression `S[i]`. To opt into this behavior, `Squares`
Expand All @@ -181,8 +181,8 @@ julia> Squares(100)[23]
529
```

Additionally, to support the syntax `S[end]`, we must define [`lastindex`](@ref) to specify the last
valid index. It is recommended to also define [`firstindex`](@ref) to specify the first valid index:
Additionally, to support the syntax `S[begin]` and `S[end]`, we must define [`firstindex`](@ref) and
[`lastindex`](@ref) to specify the first and last valid indices, respectively:

```jldoctest squaretype
julia> Base.firstindex(S::Squares) = 1
Expand Down
11 changes: 7 additions & 4 deletions doc/src/manual/strings.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ julia> """Contains "quote" characters"""
If you want to extract a character from a string, you index into it:

```jldoctest helloworldstring
julia> str[begin]
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)
julia> str[1]
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)
Expand All @@ -181,8 +184,8 @@ julia> str[end]

Many Julia objects, including strings, can be indexed with integers. The index of the first
element (the first character of a string) is returned by [`firstindex(str)`](@ref), and the index of the last element (character)
with [`lastindex(str)`](@ref). The keyword `end` can be used inside an indexing
operation as shorthand for the last index along the given dimension.
with [`lastindex(str)`](@ref). The keywords `begin` and `end` can be used inside an indexing
operation as shorthand for the first and last indices, respectively, along the given dimension.
String indexing, like most indexing in Julia, is 1-based: `firstindex` always returns `1` for any `AbstractString`.
As we will see below, however, `lastindex(str)` is *not* in general the same as `length(str)` for a string,
because some Unicode characters can occupy multiple "code units".
Expand All @@ -198,10 +201,10 @@ julia> str[end÷2]
' ': ASCII/Unicode U+0020 (category Zs: Separator, space)
```

Using an index less than 1 or greater than `end` raises an error:
Using an index less than `begin` (`1`) or greater than `end` raises an error:

```jldoctest helloworldstring
julia> str[0]
julia> str[begin-1]
ERROR: BoundsError: attempt to access String
at index [0]
[...]
Expand Down
7 changes: 4 additions & 3 deletions src/julia-parser.scm
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,10 @@
struct
module baremodule using import export))

(define initial-reserved-word? (Set initial-reserved-words))
(define initial-reserved-word?
(let ((reserved? (Set initial-reserved-words)))
(lambda (s) (and (reserved? s)
(not (and (eq? s 'begin) end-symbol)))))) ; begin == firstindex inside [...]

(define reserved-words (append initial-reserved-words '(end else elseif catch finally true false))) ;; todo: make this more complete

Expand Down Expand Up @@ -1319,8 +1322,6 @@

;; parse expressions or blocks introduced by syntactic reserved words
(define (parse-resword s word)
(if (and (eq? word 'begin) end-symbol)
(parser-depwarn s "\"begin\" inside indexing expression" ""))
(with-bindings
((expect-end-current-line (input-port-line (ts:port s))))
(with-normal-context
Expand Down
33 changes: 22 additions & 11 deletions src/julia-syntax.scm
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
(define (expand-compare-chain e)
(car (expand-vector-compare e)))

;; return the appropriate computation for an `end` symbol for indexing
;; return the appropriate computation for a `begin` or `end` symbol for indexing
;; the array `a` in the `n`th index.
;; `tuples` are a list of the splatted arguments that precede index `n`
;; `last` = is this last index?
Expand All @@ -101,20 +101,31 @@
tuples))))
`(call (top lastindex) ,a ,dimno))))

;; replace `end` for the closest ref expression, so doesn't go inside nested refs
(define (replace-end ex a n tuples last)
(define (begin-val a n tuples last)
(if (null? tuples)
(if (and last (= n 1))
`(call (top firstindex) ,a)
`(call (top first) (call (top axes) ,a ,n)))
(let ((dimno `(call (top +) ,(- n (length tuples))
,.(map (lambda (t) `(call (top length) ,t))
tuples))))
`(call (top first) (call (top axes) ,a ,dimno)))))

;; replace `begin` and `end` for the closest ref expression, so doesn't go inside nested refs
(define (replace-beginend ex a n tuples last)
(cond ((eq? ex 'end) (end-val a n tuples last))
((eq? ex 'begin) (begin-val a n tuples last))
((or (atom? ex) (quoted? ex)) ex)
((eq? (car ex) 'ref)
;; inside ref only replace within the first argument
(list* 'ref (replace-end (cadr ex) a n tuples last)
(list* 'ref (replace-beginend (cadr ex) a n tuples last)
(cddr ex)))
(else
(cons (car ex)
(map (lambda (x) (replace-end x a n tuples last))
(map (lambda (x) (replace-beginend x a n tuples last))
(cdr ex))))))

;; go through indices and replace the `end` symbol
;; go through indices and replace the `begin` or `end` symbol
;; a = array being indexed, i = list of indices
;; returns (values index-list stmts) where stmts are statements that need
;; to execute first.
Expand All @@ -133,17 +144,17 @@
(loop (cdr lst) (+ n 1)
stmts
(cons (cadr idx) tuples)
(cons `(... ,(replace-end (cadr idx) a n tuples last))
(cons `(... ,(replace-beginend (cadr idx) a n tuples last))
ret))
(let ((g (make-ssavalue)))
(loop (cdr lst) (+ n 1)
(cons `(= ,g ,(replace-end (cadr idx) a n tuples last))
(cons `(= ,g ,(replace-beginend (cadr idx) a n tuples last))
stmts)
(cons g tuples)
(cons `(... ,g) ret))))
(loop (cdr lst) (+ n 1)
stmts tuples
(cons (replace-end idx a n tuples last) ret)))))))
(cons (replace-beginend idx a n tuples last) ret)))))))

;; GF method does not need to keep decl expressions on lambda args
;; except for rest arg
Expand Down Expand Up @@ -1476,7 +1487,7 @@
(let ((a (cadr e))
(idxs (cddr e)))
(let* ((reuse (and (pair? a)
(contains (lambda (x) (eq? x 'end))
(contains (lambda (x) (or (eq? x 'begin) (eq? x 'end)))
idxs)))
(arr (if reuse (make-ssavalue) a))
(stmts (if reuse `((= ,arr ,a)) '())))
Expand All @@ -1488,7 +1499,7 @@

(define (expand-update-operator op op= lhs rhs . declT)
(cond ((and (pair? lhs) (eq? (car lhs) 'ref))
;; expand indexing inside op= first, to remove "end" and ":"
;; expand indexing inside op= first, to remove "begin", "end", and ":"
(let* ((ex (partially-expand-ref lhs))
(stmts (butlast (cdr ex)))
(refex (last (cdr ex)))
Expand Down
2 changes: 1 addition & 1 deletion test/abstractarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ function test_primitives(::Type{T}, shape, ::Type{TestAbstractArray}) where T
@test lastindex(B, 2) == lastindex(A, 2) == last(axes(B, 2))

# first(a)
@test first(B) == B[firstindex(B)] == B[1] == A[1] # TODO: use B[begin] once parser transforms it
@test first(B) == B[firstindex(B)] == B[begin] == B[1] == A[1] == A[begin]
@test firstindex(B) == firstindex(A) == first(LinearIndices(B))
@test firstindex(B, 1) == firstindex(A, 1) == first(axes(B, 1))
@test firstindex(B, 2) == firstindex(A, 2) == first(axes(B, 2))
Expand Down
7 changes: 7 additions & 0 deletions test/offsetarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ end
@test A[OffsetArray([true true; false true], A.offsets)] == [1,3,4]
@test_throws BoundsError A[[true true; false true]]

# begin, end
a0 = rand(2,3,4,2)
a = OffsetArray(a0, (-2,-3,4,5))
@test a[begin,end,end,begin] == a0[begin,end,end,begin] ==
a0[1,3,4,1] == a0[end-1,begin+2,begin+3,end-1]

# view
S = view(A, :, 3)
@test S == OffsetArray([1,2], (A.offsets[1],))
Expand Down Expand Up @@ -344,6 +350,7 @@ v2 = copy(v)
@test push!(v2, 1) === v2
@test v2[axes(v, 1)] == v
@test v2[end] == 1
@test v2[begin] == v[begin] == v[-2]
v2 = copy(v)
@test push!(v2, 2, 1) === v2
@test v2[axes(v, 1)] == v
Expand Down
Loading

0 comments on commit 8b4232b

Please sign in to comment.