The problem is that we use end to denote the end of blocks, so treating it as a symbol everywhere would lead to a lot of parsing ambiguities. That’s also why you can’t use begin ... end blocks inside indexing expressions, for example.
Another problem with this approach is that you basically end up writing a small computer algebra system and you would need a way to trace through arbitrary functions, since you don’t know the last index of the array in advance. I implemented something like that in a more limited way as tbegin and tend in CoolTensors.jl, so it’s not impossible to make this work for the most common cases, but it’s very difficult and brittle to implement this in all generality, so it’s suitable for Base.
end is a keyword that is also used to end blocks so it would be hard to also parse it as an end-of-array indices marker in general contexts. However, it would certainly be possible to define an END object that behaves very similarly:
import Base: Colon, getindex
struct End; end # singleton
const END = End()
(::Colon)(start::Integer, ::End) = EndUnitRange(start)
getindex(A::Array, r::EndUnitRange) = A[r.start:end]
# ... and other AbstractRange methods, probably…
You can then do:
julia> a = 2:END;
julia> b = [1,2,3,4,5];
julia> c = b[a]
However, a problem with this is that such a 2:END range object has little meaning except in getindex and a few similar contexts, and for all of these specialized methods would have to be defined — you can’t use it in most contexts where an ordinary range would work. (What is length(a)? What does for x in a mean?) So I’m not sure how useful it would be in practice. (But someone could certainly make a package for it if they felt otherwise.)
In contrast, the current 2:end syntax is only available in object[...] contexts where the meaning of end is unambiguous. And since end is “lowered” to lastindex(object), individual types don’t need to do anything special to support it.