Skip to content

Commit 8b4232b

Browse files
stevengjJeffBezanson
authored andcommitted
support a[begin] for a[firstindex(a)] (#33946)
* 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
1 parent f7b5faf commit 8b4232b

File tree

10 files changed

+114
-58
lines changed

10 files changed

+114
-58
lines changed

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ New language features
1111
* Function composition now supports multiple functions: `∘(f, g, h) = f ∘ g ∘ h`
1212
and splatting `∘(fs...)` for composing an iterable collection of functions ([#33568]).
1313

14+
* `a[begin]` can now be used to address the first element of an integer-indexed collection `a`.
15+
The index is computed by `firstindex(a)` ([#33946]).
16+
1417
Language changes
1518
----------------
1619

base/show.jl

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,7 +1115,7 @@ function show_unquoted_quote_expr(io::IO, @nospecialize(value), indent::Int, pre
11151115
end
11161116
else
11171117
if isa(value,Expr) && value.head === :block
1118-
show_block(io, "quote", value, indent, quote_level)
1118+
show_block(IOContext(io, beginsym=>false), "quote", value, indent, quote_level)
11191119
print(io, "end")
11201120
else
11211121
print(io, ":(")
@@ -1190,6 +1190,10 @@ function is_core_macro(arg, macro_name::AbstractString)
11901190
arg === GlobalRef(Core, Symbol(macro_name))
11911191
end
11921192

1193+
# symbol for IOContext flag signaling whether "begin" is treated
1194+
# as an ordinary symbol, which is true in indexing expressions.
1195+
const beginsym = gensym(:beginsym)
1196+
11931197
# TODO: implement interpolated strings
11941198
function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::Int = 0)
11951199
head, args, nargs = ex.head, ex.args, length(ex.args)
@@ -1324,7 +1328,7 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In
13241328
# other call-like expressions ("A[1,2]", "T{X,Y}", "f.(X,Y)")
13251329
elseif haskey(expr_calls, head) && nargs >= 1 # :ref/:curly/:calldecl/:(.)
13261330
funcargslike = head === :(.) ? args[2].args : args[2:end]
1327-
show_call(io, head, args[1], funcargslike, indent, quote_level)
1331+
show_call(head == :ref ? IOContext(io, beginsym=>true) : io, head, args[1], funcargslike, indent, quote_level)
13281332

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

13661370
elseif (head === :function || head === :macro) && nargs == 1
13671371
print(io, head, ' ')
1368-
show_unquoted(io, args[1])
1372+
show_unquoted(IOContext(io, beginsym=>false), args[1])
13691373
print(io, " end")
13701374

13711375
elseif head === :do && nargs == 2
1372-
show_unquoted(io, args[1], indent, -1, quote_level)
1376+
iob = IOContext(io, beginsym=>false)
1377+
show_unquoted(iob, args[1], indent, -1, quote_level)
13731378
print(io, " do ")
1374-
show_list(io, args[2].args[1].args, ", ", 0, 0, quote_level)
1379+
show_list(iob, args[2].args[1].args, ", ", 0, 0, quote_level)
13751380
for stmt in args[2].args[2].args
13761381
print(io, '\n', " "^(indent + indent_width))
1377-
show_unquoted(io, stmt, indent + indent_width, -1, quote_level)
1382+
show_unquoted(iob, stmt, indent + indent_width, -1, quote_level)
13781383
end
13791384
print(io, '\n', " "^indent)
13801385
print(io, "end")
13811386

13821387
# block with argument
13831388
elseif head in (:for,:while,:function,:macro,:if,:elseif,:let) && nargs==2
13841389
if Meta.isexpr(args[2], :block)
1385-
show_block(io, head, args[1], args[2], indent, quote_level)
1390+
show_block(IOContext(io, beginsym=>false), head, args[1], args[2], indent, quote_level)
13861391
else
1387-
show_block(io, head, args[1], Expr(:block, args[2]), indent, quote_level)
1392+
show_block(IOContext(io, beginsym=>false), head, args[1], Expr(:block, args[2]), indent, quote_level)
13881393
end
13891394
print(io, "end")
13901395

13911396
elseif (head === :if || head === :elseif) && nargs == 3
1392-
show_block(io, head, args[1], args[2], indent, quote_level)
1397+
iob = IOContext(io, beginsym=>false)
1398+
show_block(iob, head, args[1], args[2], indent, quote_level)
13931399
if isa(args[3],Expr) && args[3].head === :elseif
1394-
show_unquoted(io, args[3], indent, prec, quote_level)
1400+
show_unquoted(iob, args[3], indent, prec, quote_level)
13951401
else
1396-
show_block(io, "else", args[3], indent, quote_level)
1402+
show_block(iob, "else", args[3], indent, quote_level)
13971403
print(io, "end")
13981404
end
13991405

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

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

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

14141420
elseif head === :abstract && nargs == 1
14151421
print(io, "abstract type ")
1416-
show_list(io, args, ' ', indent, 0, quote_level)
1422+
show_list(IOContext(io, beginsym=>false), args, ' ', indent, 0, quote_level)
14171423
print(io, " end")
14181424

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

15171523
elseif head === :try && 3 <= nargs <= 4
1518-
show_block(io, "try", args[1], indent, quote_level)
1524+
iob = IOContext(io, beginsym=>false)
1525+
show_block(iob, "try", args[1], indent, quote_level)
15191526
if is_expr(args[3], :block)
1520-
show_block(io, "catch", args[2] === false ? Any[] : args[2], args[3], indent, quote_level)
1527+
show_block(iob, "catch", args[2] === false ? Any[] : args[2], args[3], indent, quote_level)
15211528
end
15221529
if nargs >= 4 && is_expr(args[4], :block)
1523-
show_block(io, "finally", Any[], args[4], indent, quote_level)
1530+
show_block(iob, "finally", Any[], args[4], indent, quote_level)
15241531
end
15251532
print(io, "end")
15261533

15271534
elseif head === :block
1528-
show_block(io, "begin", ex, indent, quote_level)
1529-
print(io, "end")
1535+
# print as (...; ...; ...;) inside indexing expression
1536+
if get(io, beginsym, false)
1537+
print(io, '(')
1538+
ind = indent + indent_width
1539+
for i = 1:length(ex.args)
1540+
i > 1 && print(io, ";\n", ' '^ind)
1541+
show_unquoted(io, ex.args[i], ind, -1, quote_level)
1542+
end
1543+
if length(ex.args) < 2
1544+
print(isempty(ex.args) ? "nothing;)" : ";)")
1545+
else
1546+
print(io, ')')
1547+
end
1548+
else
1549+
show_block(io, "begin", ex, indent, quote_level)
1550+
print(io, "end")
1551+
end
15301552

15311553
elseif head === :quote && nargs == 1 && isa(args[1], Symbol)
1532-
show_unquoted_quote_expr(io, args[1]::Symbol, indent, 0, quote_level+1)
1554+
show_unquoted_quote_expr(IOContext(io, beginsym=>false), args[1]::Symbol, indent, 0, quote_level+1)
15331555
elseif head === :quote && nargs == 1 && Meta.isexpr(args[1], :block)
1534-
show_block(io, "quote", Expr(:quote, args[1].args...), indent,
1556+
show_block(IOContext(io, beginsym=>false), "quote", Expr(:quote, args[1].args...), indent,
15351557
quote_level+1)
15361558
print(io, "end")
15371559
elseif head === :quote && nargs == 1
15381560
print(io, ":(")
1539-
show_unquoted(io, args[1], indent+2, 0, quote_level+1)
1561+
show_unquoted(IOContext(io, beginsym=>false), args[1], indent+2, 0, quote_level+1)
15401562
print(io, ")")
15411563
elseif head === :quote
1542-
show_block(io, "quote", ex, indent, quote_level+1)
1564+
show_block(IOContext(io, beginsym=>false), "quote", ex, indent, quote_level+1)
15431565
print(io, "end")
15441566

15451567
elseif head === :gotoifnot && nargs == 2 && isa(args[2], Int)

doc/src/manual/functions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -838,8 +838,8 @@ the results (see [Pre-allocating outputs](@ref)). A convenient syntax for this i
838838
is equivalent to `broadcast!(identity, X, ...)` except that, as above, the `broadcast!` loop is
839839
fused with any nested "dot" calls. For example, `X .= sin.(Y)` is equivalent to `broadcast!(sin, X, Y)`,
840840
overwriting `X` with `sin.(Y)` in-place. If the left-hand side is an array-indexing expression,
841-
e.g. `X[2:end] .= sin.(Y)`, then it translates to `broadcast!` on a `view`, e.g.
842-
`broadcast!(sin, view(X, 2:lastindex(X)), Y)`,
841+
e.g. `X[begin+1:end] .= sin.(Y)`, then it translates to `broadcast!` on a `view`, e.g.
842+
`broadcast!(sin, view(X, firstindex(X)+1:lastindex(X)), Y)`,
843843
so that the left-hand side is updated in-place.
844844

845845
Since adding dots to many operations and function calls in an expression

doc/src/manual/interfaces.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,8 @@ julia> collect(Iterators.reverse(Squares(4)))
164164
|:-------------------- |:-------------------------------- |
165165
| `getindex(X, i)` | `X[i]`, indexed element access |
166166
| `setindex!(X, v, i)` | `X[i] = v`, indexed assignment |
167-
| `firstindex(X)` | The first index |
168-
| `lastindex(X)` | The last index, used in `X[end]` |
167+
| `firstindex(X)` | The first index, used in `X[begin]` |
168+
| `lastindex(X)` | The last index, used in `X[end]` |
169169

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

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

187187
```jldoctest squaretype
188188
julia> Base.firstindex(S::Squares) = 1

doc/src/manual/strings.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ julia> """Contains "quote" characters"""
169169
If you want to extract a character from a string, you index into it:
170170

171171
```jldoctest helloworldstring
172+
julia> str[begin]
173+
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)
174+
172175
julia> str[1]
173176
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)
174177
@@ -181,8 +184,8 @@ julia> str[end]
181184

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

201-
Using an index less than 1 or greater than `end` raises an error:
204+
Using an index less than `begin` (`1`) or greater than `end` raises an error:
202205

203206
```jldoctest helloworldstring
204-
julia> str[0]
207+
julia> str[begin-1]
205208
ERROR: BoundsError: attempt to access String
206209
at index [0]
207210
[...]

src/julia-parser.scm

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,10 @@
160160
struct
161161
module baremodule using import export))
162162

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

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

@@ -1319,8 +1322,6 @@
13191322

13201323
;; parse expressions or blocks introduced by syntactic reserved words
13211324
(define (parse-resword s word)
1322-
(if (and (eq? word 'begin) end-symbol)
1323-
(parser-depwarn s "\"begin\" inside indexing expression" ""))
13241325
(with-bindings
13251326
((expect-end-current-line (input-port-line (ts:port s))))
13261327
(with-normal-context

src/julia-syntax.scm

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
(define (expand-compare-chain e)
8787
(car (expand-vector-compare e)))
8888

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

104-
;; replace `end` for the closest ref expression, so doesn't go inside nested refs
105-
(define (replace-end ex a n tuples last)
104+
(define (begin-val a n tuples last)
105+
(if (null? tuples)
106+
(if (and last (= n 1))
107+
`(call (top firstindex) ,a)
108+
`(call (top first) (call (top axes) ,a ,n)))
109+
(let ((dimno `(call (top +) ,(- n (length tuples))
110+
,.(map (lambda (t) `(call (top length) ,t))
111+
tuples))))
112+
`(call (top first) (call (top axes) ,a ,dimno)))))
113+
114+
;; replace `begin` and `end` for the closest ref expression, so doesn't go inside nested refs
115+
(define (replace-beginend ex a n tuples last)
106116
(cond ((eq? ex 'end) (end-val a n tuples last))
117+
((eq? ex 'begin) (begin-val a n tuples last))
107118
((or (atom? ex) (quoted? ex)) ex)
108119
((eq? (car ex) 'ref)
109120
;; inside ref only replace within the first argument
110-
(list* 'ref (replace-end (cadr ex) a n tuples last)
121+
(list* 'ref (replace-beginend (cadr ex) a n tuples last)
111122
(cddr ex)))
112123
(else
113124
(cons (car ex)
114-
(map (lambda (x) (replace-end x a n tuples last))
125+
(map (lambda (x) (replace-beginend x a n tuples last))
115126
(cdr ex))))))
116127

117-
;; go through indices and replace the `end` symbol
128+
;; go through indices and replace the `begin` or `end` symbol
118129
;; a = array being indexed, i = list of indices
119130
;; returns (values index-list stmts) where stmts are statements that need
120131
;; to execute first.
@@ -133,17 +144,17 @@
133144
(loop (cdr lst) (+ n 1)
134145
stmts
135146
(cons (cadr idx) tuples)
136-
(cons `(... ,(replace-end (cadr idx) a n tuples last))
147+
(cons `(... ,(replace-beginend (cadr idx) a n tuples last))
137148
ret))
138149
(let ((g (make-ssavalue)))
139150
(loop (cdr lst) (+ n 1)
140-
(cons `(= ,g ,(replace-end (cadr idx) a n tuples last))
151+
(cons `(= ,g ,(replace-beginend (cadr idx) a n tuples last))
141152
stmts)
142153
(cons g tuples)
143154
(cons `(... ,g) ret))))
144155
(loop (cdr lst) (+ n 1)
145156
stmts tuples
146-
(cons (replace-end idx a n tuples last) ret)))))))
157+
(cons (replace-beginend idx a n tuples last) ret)))))))
147158

148159
;; GF method does not need to keep decl expressions on lambda args
149160
;; except for rest arg
@@ -1476,7 +1487,7 @@
14761487
(let ((a (cadr e))
14771488
(idxs (cddr e)))
14781489
(let* ((reuse (and (pair? a)
1479-
(contains (lambda (x) (eq? x 'end))
1490+
(contains (lambda (x) (or (eq? x 'begin) (eq? x 'end)))
14801491
idxs)))
14811492
(arr (if reuse (make-ssavalue) a))
14821493
(stmts (if reuse `((= ,arr ,a)) '())))
@@ -1488,7 +1499,7 @@
14881499

14891500
(define (expand-update-operator op op= lhs rhs . declT)
14901501
(cond ((and (pair? lhs) (eq? (car lhs) 'ref))
1491-
;; expand indexing inside op= first, to remove "end" and ":"
1502+
;; expand indexing inside op= first, to remove "begin", "end", and ":"
14921503
(let* ((ex (partially-expand-ref lhs))
14931504
(stmts (butlast (cdr ex)))
14941505
(refex (last (cdr ex)))

test/abstractarray.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ function test_primitives(::Type{T}, shape, ::Type{TestAbstractArray}) where T
484484
@test lastindex(B, 2) == lastindex(A, 2) == last(axes(B, 2))
485485

486486
# first(a)
487-
@test first(B) == B[firstindex(B)] == B[1] == A[1] # TODO: use B[begin] once parser transforms it
487+
@test first(B) == B[firstindex(B)] == B[begin] == B[1] == A[1] == A[begin]
488488
@test firstindex(B) == firstindex(A) == first(LinearIndices(B))
489489
@test firstindex(B, 1) == firstindex(A, 1) == first(axes(B, 1))
490490
@test firstindex(B, 2) == firstindex(A, 2) == first(axes(B, 2))

test/offsetarray.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ end
116116
@test A[OffsetArray([true true; false true], A.offsets)] == [1,3,4]
117117
@test_throws BoundsError A[[true true; false true]]
118118

119+
# begin, end
120+
a0 = rand(2,3,4,2)
121+
a = OffsetArray(a0, (-2,-3,4,5))
122+
@test a[begin,end,end,begin] == a0[begin,end,end,begin] ==
123+
a0[1,3,4,1] == a0[end-1,begin+2,begin+3,end-1]
124+
119125
# view
120126
S = view(A, :, 3)
121127
@test S == OffsetArray([1,2], (A.offsets[1],))
@@ -344,6 +350,7 @@ v2 = copy(v)
344350
@test push!(v2, 1) === v2
345351
@test v2[axes(v, 1)] == v
346352
@test v2[end] == 1
353+
@test v2[begin] == v[begin] == v[-2]
347354
v2 = copy(v)
348355
@test push!(v2, 2, 1) === v2
349356
@test v2[axes(v, 1)] == v

0 commit comments

Comments
 (0)