Skip to content

Commit 8407c59

Browse files
authored
allow unicode modifiers after ' (JuliaLang#37247)
1 parent 9d6da9c commit 8407c59

File tree

6 files changed

+72
-21
lines changed

6 files changed

+72
-21
lines changed

NEWS.md

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ New language features
2525
can now be used to rename imported modules and identifiers ([#1255]).
2626
* Unsigned literals (starting with `0x`) which are too big to fit in an `UInt128` object
2727
are now interpreted as `BigInt` ([#23546]).
28+
* The postfix conjugate transpose operator `'` now accepts Unicode modifiers as
29+
suffixes, so e.g. `a'ᵀ` is parsed as `var"'ᵀ"(a)`, which can be defined by the
30+
user. `a'ᵀ` parsed as `a' * ᵀ` before, so this is a minor change ([#37247]).
2831

2932
Language changes
3033
----------------

base/show.jl

+40-14
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,10 @@ function isidentifier(s::AbstractString)
11311131
end
11321132
isidentifier(s::Symbol) = isidentifier(string(s))
11331133

1134+
is_op_suffix_char(c::AbstractChar) = ccall(:jl_op_suffix_char, Cint, (UInt32,), c) != 0
1135+
1136+
_isoperator(s) = ccall(:jl_is_operator, Cint, (Cstring,), s) != 0
1137+
11341138
"""
11351139
isoperator(s::Symbol)
11361140
@@ -1142,7 +1146,7 @@ julia> Base.isoperator(:+), Base.isoperator(:f)
11421146
(true, false)
11431147
```
11441148
"""
1145-
isoperator(s::Union{Symbol,AbstractString}) = ccall(:jl_is_operator, Cint, (Cstring,), s) != 0
1149+
isoperator(s::Union{Symbol,AbstractString}) = _isoperator(s) || ispostfixoperator(s)
11461150

11471151
"""
11481152
isunaryoperator(s::Symbol)
@@ -1169,7 +1173,26 @@ julia> Base.isbinaryoperator(:-), Base.isbinaryoperator(:√), Base.isbinaryoper
11691173
(true, false, false)
11701174
```
11711175
"""
1172-
isbinaryoperator(s::Symbol) = isoperator(s) && (!isunaryoperator(s) || is_unary_and_binary_operator(s))
1176+
function isbinaryoperator(s::Symbol)
1177+
return _isoperator(s) && (!isunaryoperator(s) || is_unary_and_binary_operator(s)) &&
1178+
s !== Symbol("'")
1179+
end
1180+
1181+
"""
1182+
ispostfixoperator(s::Union{Symbol,AbstractString})
1183+
1184+
Return `true` if the symbol can be used as a postfix operator, `false` otherwise.
1185+
1186+
# Examples
1187+
```jldoctest
1188+
julia> Base.ispostfixoperator(Symbol("'")), Base.ispostfixoperator(Symbol("'ᵀ")), Base.ispostfixoperator(:-)
1189+
(true, true, false)
1190+
```
1191+
"""
1192+
function ispostfixoperator(s::Union{Symbol,AbstractString})
1193+
s = String(s)
1194+
return startswith(s, '\'') && all(is_op_suffix_char, SubString(s, 2))
1195+
end
11731196

11741197
"""
11751198
operator_precedence(s::Symbol)
@@ -1594,6 +1617,20 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In
15941617
end
15951618
head !== :row && print(io, cl)
15961619

1620+
# transpose
1621+
elseif (head === Symbol("'") && nargs == 1) || (
1622+
# ' with unicode suffix is a call expression
1623+
head === :call && nargs == 2 && args[1] isa Symbol &&
1624+
ispostfixoperator(args[1]) && args[1] !== Symbol("'")
1625+
)
1626+
op, arg1 = head === Symbol("'") ? (head, args[1]) : (args[1], args[2])
1627+
if isa(arg1, Expr) || (isa(arg1, Symbol) && isoperator(arg1))
1628+
show_enclosed_list(io, '(', [arg1], ", ", ')', indent, 0)
1629+
else
1630+
show_unquoted(io, arg1, indent, 0, quote_level)
1631+
end
1632+
print(io, op)
1633+
15971634
# function call
15981635
elseif head === :call && nargs >= 1
15991636
func = args[1]
@@ -1622,7 +1659,7 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In
16221659
end
16231660

16241661
# unary operator (i.e. "!z")
1625-
elseif isa(func,Symbol) && func in uni_ops && length(func_args) == 1
1662+
elseif isa(func,Symbol) && length(func_args) == 1 && func in uni_ops
16261663
show_unquoted(io, func, indent, 0, quote_level)
16271664
arg1 = func_args[1]
16281665
if isa(arg1, Expr) || (isa(arg1, Symbol) && isoperator(arg1))
@@ -1959,17 +1996,6 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In
19591996
parens && print(io, ")")
19601997
end
19611998

1962-
# transpose
1963-
elseif head === Symbol('\'') && nargs == 1
1964-
if isa(args[1], Symbol)
1965-
show_unquoted(io, args[1], 0, 0, quote_level)
1966-
else
1967-
print(io, "(")
1968-
show_unquoted(io, args[1], 0, 0, quote_level)
1969-
print(io, ")")
1970-
end
1971-
print(io, head)
1972-
19731999
# `where` syntax
19742000
elseif head === :where && nargs > 1
19752001
parens = 1 <= prec

src/flisp/julia_extensions.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ JL_DLLEXPORT int jl_id_char(uint32_t wc)
152152
#include "julia_opsuffs.h"
153153

154154
// chars that can follow an operator (e.g. +) and be parsed as part of the operator
155-
int jl_op_suffix_char(uint32_t wc)
155+
JL_DLLEXPORT int jl_op_suffix_char(uint32_t wc)
156156
{
157157
static htable_t jl_opsuffs; // XXX: requires uv_once
158158
if (!jl_opsuffs.size) { // initialize hash table of suffixes

src/julia-parser.scm

+12-6
Original file line numberDiff line numberDiff line change
@@ -228,12 +228,12 @@
228228

229229
(define (op-or-sufchar? c) (or (op-suffix-char? c) (opchar? c)))
230230

231-
(define (read-operator port c)
232-
(if (and (eqv? c #\*) (eqv? (peek-char port) #\*))
231+
(define (read-operator port c0 (postfix? #f))
232+
(if (and (eqv? c0 #\*) (eqv? (peek-char port) #\*))
233233
(error "use \"x^y\" instead of \"x**y\" for exponentiation, and \"x...\" instead of \"**x\" for splatting."))
234234
(if (or (eof-object? (peek-char port)) (not (op-or-sufchar? (peek-char port))))
235-
(symbol (string c)) ; 1-char operator
236-
(let ((str (let loop ((str (string c))
235+
(symbol (string c0)) ; 1-char operator
236+
(let ((str (let loop ((str (string c0))
237237
(c (peek-char port))
238238
(in-suffix? #f))
239239
(if (eof-object? c)
@@ -257,7 +257,10 @@
257257
(and (or (eq? opsym '<-) (eq? opsym '.<-))
258258
(read-char port)
259259
(begin0 (eqv? (peek-char port) #\-)
260-
(io.ungetc port #\-))))
260+
(io.ungetc port #\-)))
261+
;; consume suffixes after ', only if parsing a call chain
262+
;; otherwise 'ᵀ' would parse as (|'| |'ᵀ|)
263+
(and postfix? (eqv? c0 #\') sufchar?))
261264
(begin (read-char port)
262265
(loop newop (peek-char port) sufchar?))
263266
str))
@@ -1244,7 +1247,10 @@
12441247
(if (not (ts:space? s))
12451248
(begin
12461249
(take-token s)
1247-
(loop (list t ex)))
1250+
(let ((t (read-operator (ts:port s) #\' #t)))
1251+
(loop (if (eq? t '|'|)
1252+
(list t ex)
1253+
(list 'call t ex)))))
12481254
ex))
12491255
((|.'|) (error "the \".'\" operator is discontinued"))
12501256
((#\{ )

test/show.jl

+9
Original file line numberDiff line numberDiff line change
@@ -2028,3 +2028,12 @@ end
20282028

20292029
@test sprint(show, :(./)) == ":((./))"
20302030
@test sprint(show, :((.|).(.&, b))) == ":((.|).((.&), b))"
2031+
2032+
@test sprint(show, :(a'ᵀ)) == ":(a'ᵀ)"
2033+
@test sprint(show, :((+)')) == ":((+)')"
2034+
for s in (Symbol("'"), Symbol("'⁻¹"))
2035+
@test Base.isoperator(s)
2036+
@test !Base.isunaryoperator(s)
2037+
@test !Base.isbinaryoperator(s)
2038+
@test Base.ispostfixoperator(s)
2039+
end

test/syntax.jl

+7
Original file line numberDiff line numberDiff line change
@@ -2445,3 +2445,10 @@ end
24452445
import .TestImportAs.Mod2 as M2
24462446
@test !@isdefined(Mod2)
24472447
@test M2 === TestImportAs.Mod2
2448+
2449+
@testset "unicode modifiers after '" begin
2450+
@test Meta.parse("a'ᵀ") == Expr(:call, Symbol("'ᵀ"), :a)
2451+
@test Meta.parse("a'⁻¹") == Expr(:call, Symbol("'⁻¹"), :a)
2452+
@test Meta.parse("a'ᵀb") == Expr(:call, :*, Expr(:call, Symbol("'ᵀ"), :a), :b)
2453+
@test Meta.parse("a'⁻¹b") == Expr(:call, :*, Expr(:call, Symbol("'⁻¹"), :a), :b)
2454+
end

0 commit comments

Comments
 (0)