From 8407c596d9017b897e014c506bb10f7cfa67926b Mon Sep 17 00:00:00 2001 From: Simeon Schaub Date: Fri, 16 Oct 2020 20:10:06 +0200 Subject: [PATCH] allow unicode modifiers after ' (#37247) --- NEWS.md | 3 ++ base/show.jl | 54 ++++++++++++++++++++++++++---------- src/flisp/julia_extensions.c | 2 +- src/julia-parser.scm | 18 ++++++++---- test/show.jl | 9 ++++++ test/syntax.jl | 7 +++++ 6 files changed, 72 insertions(+), 21 deletions(-) diff --git a/NEWS.md b/NEWS.md index f14419b4fb854..b3da572b13bf4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -25,6 +25,9 @@ New language features can now be used to rename imported modules and identifiers ([#1255]). * Unsigned literals (starting with `0x`) which are too big to fit in an `UInt128` object are now interpreted as `BigInt` ([#23546]). +* The postfix conjugate transpose operator `'` now accepts Unicode modifiers as + suffixes, so e.g. `a'ᵀ` is parsed as `var"'ᵀ"(a)`, which can be defined by the + user. `a'ᵀ` parsed as `a' * ᵀ` before, so this is a minor change ([#37247]). Language changes ---------------- diff --git a/base/show.jl b/base/show.jl index fdc044ba984b0..fbc3b3eda8548 100644 --- a/base/show.jl +++ b/base/show.jl @@ -1131,6 +1131,10 @@ function isidentifier(s::AbstractString) end isidentifier(s::Symbol) = isidentifier(string(s)) +is_op_suffix_char(c::AbstractChar) = ccall(:jl_op_suffix_char, Cint, (UInt32,), c) != 0 + +_isoperator(s) = ccall(:jl_is_operator, Cint, (Cstring,), s) != 0 + """ isoperator(s::Symbol) @@ -1142,7 +1146,7 @@ julia> Base.isoperator(:+), Base.isoperator(:f) (true, false) ``` """ -isoperator(s::Union{Symbol,AbstractString}) = ccall(:jl_is_operator, Cint, (Cstring,), s) != 0 +isoperator(s::Union{Symbol,AbstractString}) = _isoperator(s) || ispostfixoperator(s) """ isunaryoperator(s::Symbol) @@ -1169,7 +1173,26 @@ julia> Base.isbinaryoperator(:-), Base.isbinaryoperator(:√), Base.isbinaryoper (true, false, false) ``` """ -isbinaryoperator(s::Symbol) = isoperator(s) && (!isunaryoperator(s) || is_unary_and_binary_operator(s)) +function isbinaryoperator(s::Symbol) + return _isoperator(s) && (!isunaryoperator(s) || is_unary_and_binary_operator(s)) && + s !== Symbol("'") +end + +""" + ispostfixoperator(s::Union{Symbol,AbstractString}) + +Return `true` if the symbol can be used as a postfix operator, `false` otherwise. + +# Examples +```jldoctest +julia> Base.ispostfixoperator(Symbol("'")), Base.ispostfixoperator(Symbol("'ᵀ")), Base.ispostfixoperator(:-) +(true, true, false) +``` +""" +function ispostfixoperator(s::Union{Symbol,AbstractString}) + s = String(s) + return startswith(s, '\'') && all(is_op_suffix_char, SubString(s, 2)) +end """ operator_precedence(s::Symbol) @@ -1594,6 +1617,20 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In end head !== :row && print(io, cl) + # transpose + elseif (head === Symbol("'") && nargs == 1) || ( + # ' with unicode suffix is a call expression + head === :call && nargs == 2 && args[1] isa Symbol && + ispostfixoperator(args[1]) && args[1] !== Symbol("'") + ) + op, arg1 = head === Symbol("'") ? (head, args[1]) : (args[1], args[2]) + if isa(arg1, Expr) || (isa(arg1, Symbol) && isoperator(arg1)) + show_enclosed_list(io, '(', [arg1], ", ", ')', indent, 0) + else + show_unquoted(io, arg1, indent, 0, quote_level) + end + print(io, op) + # function call elseif head === :call && nargs >= 1 func = args[1] @@ -1622,7 +1659,7 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In end # unary operator (i.e. "!z") - elseif isa(func,Symbol) && func in uni_ops && length(func_args) == 1 + elseif isa(func,Symbol) && length(func_args) == 1 && func in uni_ops show_unquoted(io, func, indent, 0, quote_level) arg1 = func_args[1] 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 parens && print(io, ")") end - # transpose - elseif head === Symbol('\'') && nargs == 1 - if isa(args[1], Symbol) - show_unquoted(io, args[1], 0, 0, quote_level) - else - print(io, "(") - show_unquoted(io, args[1], 0, 0, quote_level) - print(io, ")") - end - print(io, head) - # `where` syntax elseif head === :where && nargs > 1 parens = 1 <= prec diff --git a/src/flisp/julia_extensions.c b/src/flisp/julia_extensions.c index 11f17309401c2..bad9c53d3f5d9 100644 --- a/src/flisp/julia_extensions.c +++ b/src/flisp/julia_extensions.c @@ -152,7 +152,7 @@ JL_DLLEXPORT int jl_id_char(uint32_t wc) #include "julia_opsuffs.h" // chars that can follow an operator (e.g. +) and be parsed as part of the operator -int jl_op_suffix_char(uint32_t wc) +JL_DLLEXPORT int jl_op_suffix_char(uint32_t wc) { static htable_t jl_opsuffs; // XXX: requires uv_once if (!jl_opsuffs.size) { // initialize hash table of suffixes diff --git a/src/julia-parser.scm b/src/julia-parser.scm index 255a8cfbd4ee8..653daff6b39d7 100644 --- a/src/julia-parser.scm +++ b/src/julia-parser.scm @@ -228,12 +228,12 @@ (define (op-or-sufchar? c) (or (op-suffix-char? c) (opchar? c))) -(define (read-operator port c) - (if (and (eqv? c #\*) (eqv? (peek-char port) #\*)) +(define (read-operator port c0 (postfix? #f)) + (if (and (eqv? c0 #\*) (eqv? (peek-char port) #\*)) (error "use \"x^y\" instead of \"x**y\" for exponentiation, and \"x...\" instead of \"**x\" for splatting.")) (if (or (eof-object? (peek-char port)) (not (op-or-sufchar? (peek-char port)))) - (symbol (string c)) ; 1-char operator - (let ((str (let loop ((str (string c)) + (symbol (string c0)) ; 1-char operator + (let ((str (let loop ((str (string c0)) (c (peek-char port)) (in-suffix? #f)) (if (eof-object? c) @@ -257,7 +257,10 @@ (and (or (eq? opsym '<-) (eq? opsym '.<-)) (read-char port) (begin0 (eqv? (peek-char port) #\-) - (io.ungetc port #\-)))) + (io.ungetc port #\-))) + ;; consume suffixes after ', only if parsing a call chain + ;; otherwise 'ᵀ' would parse as (|'| |'ᵀ|) + (and postfix? (eqv? c0 #\') sufchar?)) (begin (read-char port) (loop newop (peek-char port) sufchar?)) str)) @@ -1244,7 +1247,10 @@ (if (not (ts:space? s)) (begin (take-token s) - (loop (list t ex))) + (let ((t (read-operator (ts:port s) #\' #t))) + (loop (if (eq? t '|'|) + (list t ex) + (list 'call t ex))))) ex)) ((|.'|) (error "the \".'\" operator is discontinued")) ((#\{ ) diff --git a/test/show.jl b/test/show.jl index 3945bef2215d5..65f3a3f5f4d85 100644 --- a/test/show.jl +++ b/test/show.jl @@ -2028,3 +2028,12 @@ end @test sprint(show, :(./)) == ":((./))" @test sprint(show, :((.|).(.&, b))) == ":((.|).((.&), b))" + +@test sprint(show, :(a'ᵀ)) == ":(a'ᵀ)" +@test sprint(show, :((+)')) == ":((+)')" +for s in (Symbol("'"), Symbol("'⁻¹")) + @test Base.isoperator(s) + @test !Base.isunaryoperator(s) + @test !Base.isbinaryoperator(s) + @test Base.ispostfixoperator(s) +end diff --git a/test/syntax.jl b/test/syntax.jl index 6e39aaf448670..6af0b139026d0 100644 --- a/test/syntax.jl +++ b/test/syntax.jl @@ -2445,3 +2445,10 @@ end import .TestImportAs.Mod2 as M2 @test !@isdefined(Mod2) @test M2 === TestImportAs.Mod2 + +@testset "unicode modifiers after '" begin + @test Meta.parse("a'ᵀ") == Expr(:call, Symbol("'ᵀ"), :a) + @test Meta.parse("a'⁻¹") == Expr(:call, Symbol("'⁻¹"), :a) + @test Meta.parse("a'ᵀb") == Expr(:call, :*, Expr(:call, Symbol("'ᵀ"), :a), :b) + @test Meta.parse("a'⁻¹b") == Expr(:call, :*, Expr(:call, Symbol("'⁻¹"), :a), :b) +end