Skip to content
Open
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
28 changes: 25 additions & 3 deletions base/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1555,6 +1555,11 @@ Return an integer representing the precedence of operator `s`, relative to
other operators. Higher-numbered operators take precedence over lower-numbered
operators. Return `0` if `s` is not a valid operator.

Parsing is primarily determined by operator precedence. For operators with identical
operator precedence parsing depends on their associativity.

See also [`Base.operator_associativity`](@ref).

# Examples
```jldoctest
julia> Base.operator_precedence(:+), Base.operator_precedence(:*), Base.operator_precedence(:.)
Expand All @@ -1581,6 +1586,11 @@ Return a symbol representing the associativity of operator `s`. Left- and right-
operators return `:left` and `:right`, respectively. Return `:none` if `s` is non-associative
or an invalid operator.

Parsing is primarily determined by operator precedence. For operators with identical
operator precedence parsing depends on their associativity.

See also [`Base.operator_precedence`](@ref).

# Examples
```jldoctest
julia> Base.operator_associativity(:-), Base.operator_associativity(:+), Base.operator_associativity(:^)
Expand All @@ -1589,13 +1599,25 @@ julia> Base.operator_associativity(:-), Base.operator_associativity(:+), Base.op
julia> Base.operator_associativity(:⊗), Base.operator_associativity(:sin), Base.operator_associativity(:→)
(:left, :none, :right)
```

# Extended help
For binary operators `⊙` and `⊡` of identical operator precedence parsing depends on their
associativity as follows:

| Associativity | `x ⊙ y ⊡ z` is parsed as |
|:-------------:|:------------------------:|
| `:left` | `(x ⊙ y) ⊡ z` |
| `:none` | `x ⊙ y ⊡ z` |
| `:right` | `x ⊙ (y ⊡ z)` |

`⊙` and `⊡` can be the same operator. A difference in parsing behavior does not imply a
different result of the expression.
"""
function operator_associativity(s::Symbol)
if operator_precedence(s) in (prec_arrow, prec_assignment, prec_control_flow, prec_pair, prec_power) ||
(isunaryoperator(s) && !is_unary_and_binary_operator(s)) ||
(s === :<| || s === :|| || s == :?)
(isunaryoperator(s) && !is_unary_and_binary_operator(s)) || s in (:<|, :||, :?, :->, :🢲)
return :right
elseif operator_precedence(s) in (0, prec_comparison) || s in (:+, :++, :*)
elseif operator_precedence(s) in (0, prec_comparison) && s != :where || s in (:+, :++, :*, :(:))
return :none
end
return :left
Expand Down
3 changes: 3 additions & 0 deletions doc/src/manual/mathematical-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,9 @@ Julia applies the following order and associativity of operations, from highest
The operators `+`, `++` and `*` are non-associative. `a + b + c` is parsed as `+(a, b, c)` not `+(+(a, b),
c)`. However, the fallback methods for `+(a, b, c, d...)` and `*(a, b, c, d...)` both default to left-associative evaluation.

Parsing is primarily determined by operator precedence. For operators with identical
operator precedence parsing depends on their associativity.

For a complete list of *every* Julia operator's precedence, see the top of this file:
[`src/julia-parser.scm`](https://github.com/JuliaLang/julia/blob/master/src/julia-parser.scm). Note that some of the operators there are not defined
in the `Base` module but may be given definitions by standard libraries, packages or user code.
Expand Down
31 changes: 31 additions & 0 deletions test/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,37 @@ let ex4 = Expr(:call, :(:), 1, 2, 3, 4),
@test eval(Meta.parse(repr(ex1))) == ex1
end


# Test associativity of operators
kind_int(x) = Base.JuliaSyntax._kind_str_to_int[string(x)]
kind_sym(x) = Symbol(Base.JuliaSyntax._kind_int_to_str[x])
# Omit :. because it is parsed differently and does not need to be excluded from testing
iscolonoperator(op) = kind_int(":") < kind_int(op) <= kind_int("END_COLON")
isconditionaloperator(op) = kind_int("BEGIN_CONDITIONAL") <= kind_int(op) <= kind_int("END_CONDITIONAL")
iswordoperator(op) = op |> string |> Base.JuliaSyntax.Kind |> Base.JuliaSyntax.is_word_operator
isclosedbinaryoperator(op) = !Base.isunaryoperator(op) && !iscolonoperator(op) &&
!isconditionaloperator(op) && string(op) ∉ ("op=", "'", ".'")
space(op) = iswordoperator(op) || op == :🢲 ? Symbol(" $op ") : op

function test_associativity(op)
assoc = Base.operator_associativity(op)
# :. does not allow space around the operator, but some operators need it.
# Therefore, add space here for those needing it and omit it the line after.
op = space(op)
left, none, right = ("(x$(op)y)$(op)z", "x$(op)y$(op)z", "x$(op)(y$(op)z)") .|> Meta.parse

assoc == :left && @test left == none != right
assoc == :none && @test left != none != right
assoc == :right && @test left != none == right
end

@testset "associativity" begin
ops = (kind_sym(i) for i in kind_int("BEGIN_ASSIGNMENTS") : kind_int("END_OPS"))
[test_associativity(op) for op in ops if isclosedbinaryoperator(op)]
return nothing
end


# Complex

# Meta.parse(repr(:(...))) returns a double-quoted block, so we need to eval twice to unquote it
Expand Down