diff --git a/NEWS.md b/NEWS.md index 537e8a8fe3ec4..7534f707ad006 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 ---------------- diff --git a/base/show.jl b/base/show.jl index 07fe19ec7044c..bf607554cddfe 100644 --- a/base/show.jl +++ b/base/show.jl @@ -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, ":(") @@ -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) @@ -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 @@ -1360,21 +1364,22 @@ 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") @@ -1382,28 +1387,29 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In # 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 @@ -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") @@ -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) diff --git a/doc/src/manual/functions.md b/doc/src/manual/functions.md index 35a5862efa210..47da94ec9b265 100644 --- a/doc/src/manual/functions.md +++ b/doc/src/manual/functions.md @@ -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 diff --git a/doc/src/manual/interfaces.md b/doc/src/manual/interfaces.md index ab2aa4777158e..0fd19da754bbe 100644 --- a/doc/src/manual/interfaces.md +++ b/doc/src/manual/interfaces.md @@ -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` @@ -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 diff --git a/doc/src/manual/strings.md b/doc/src/manual/strings.md index bc9c7218f3151..dcbc3d0b81bd9 100644 --- a/doc/src/manual/strings.md +++ b/doc/src/manual/strings.md @@ -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) @@ -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". @@ -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] [...] diff --git a/src/julia-parser.scm b/src/julia-parser.scm index 30cedb67d6dd4..f4d661bddac6f 100644 --- a/src/julia-parser.scm +++ b/src/julia-parser.scm @@ -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 @@ -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 diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index 4209217f2474c..6e2cd9a21e674 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -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? @@ -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. @@ -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 @@ -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)) '()))) @@ -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))) diff --git a/test/abstractarray.jl b/test/abstractarray.jl index 8923c5a90105d..8d0c2617d78f0 100644 --- a/test/abstractarray.jl +++ b/test/abstractarray.jl @@ -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)) diff --git a/test/offsetarray.jl b/test/offsetarray.jl index 8856fb9100c5b..e38851f35624c 100644 --- a/test/offsetarray.jl +++ b/test/offsetarray.jl @@ -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],)) @@ -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 diff --git a/test/show.jl b/test/show.jl index 67ca21d030db1..f128f2c21778f 100644 --- a/test/show.jl +++ b/test/show.jl @@ -67,16 +67,18 @@ function test_repr(x::String, remove_linenums::Bool = false) x1 = Meta.parse(x) x2 = eval(Meta.parse(repr(x1))) x3 = eval(Meta.parse(repr(x2))) - if ! (x1 == x2 == x3) - error(string( - "\nrepr test (Rule 2) failed:", - "\noriginal: ", x, - "\n\npreparsed: ", x1, "\n", sprint(dump, x1), - "\n\nparsed: ", x2, "\n", sprint(dump, x2), - "\n\nreparsed: ", x3, "\n", sprint(dump, x3), - "\n\n")) + if !remove_linenums + if ! (x1 == x2 == x3) + error(string( + "\nrepr test (Rule 2) failed:", + "\noriginal: ", x, + "\n\npreparsed: ", x1, "\n", sprint(dump, x1), + "\n\nparsed: ", x2, "\n", sprint(dump, x2), + "\n\nreparsed: ", x3, "\n", sprint(dump, x3), + "\n\n")) + end + @test x1 == x2 == x3 end - @test x1 == x2 == x3 x4 = Base.remove_linenums!(Meta.parse(x)) x5 = eval(Base.remove_linenums!(Meta.parse(repr(x4)))) @@ -1953,3 +1955,10 @@ end @test sprint(show, Symbol("true")) == "Symbol(\"true\")" @test sprint(show, Symbol(false)) == "Symbol(\"false\")" @test sprint(show, Symbol("false")) == "Symbol(\"false\")" + +# begin/end indices +@weak_test_repr "a[begin, end, (begin; end)]" +@test repr(Base.remove_linenums!(:(a[begin, end, (begin; end)]))) == ":(a[begin, end, (begin;\n end)])" +@weak_test_repr "a[begin, end, let x=1; (x+1;); end]" +@test repr(Base.remove_linenums!(:(a[begin, end, let x=1; (x+1;); end]))) == + ":(a[begin, end, let x = 1\n begin\n x + 1\n end\n end])"