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
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 @@ -1113,7 +1113,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 @@ -1188,6 +1188,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 @@ -1322,7 +1326,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 @@ -1358,50 +1362,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 @@ -1411,7 +1417,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 @@ -1510,31 +1516,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)))
Copy link
Member

Choose a reason for hiding this comment

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

I missed this, but it's a bit of a shame that this isn't firstindex(a, n) to mirror lastindex(a, n).

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds like a good non-breaking PR?

(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