Skip to content

Commit fb04178

Browse files
c42fJeffBezanson
authored andcommitted
var"ident" syntax for non-standard identifiers (JuliaLang#32408)
Allow identifiers like Symbol("#example#") to be represented in julia code with the syntax `var"#example#"`. This needed support in the parser rather than being a string macro so that it could be used in any context where a normal identifier is allowed. Show nonstandard identifiers in Exprs with var"ident" syntax
1 parent c05864e commit fb04178

File tree

8 files changed

+141
-75
lines changed

8 files changed

+141
-75
lines changed

NEWS.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ New language features
1212
Language changes
1313
----------------
1414

15+
* The syntax `var"#example#"` is used to print and parse non-standard variable names ([#32408]).
1516

1617
Multi-threading changes
1718
-----------------------

base/docs/basedocs.jl

+23
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,29 @@ using the syntax `T{p1, p2, ...}`.
934934
"""
935935
kw"where"
936936

937+
"""
938+
var
939+
940+
The syntax `var"#example#"` refers to a variable named `Symbol("#example#")`,
941+
even though `#example#` is not a valid Julia identifier name.
942+
943+
This can be useful for interoperability with programming languages which have
944+
different rules for the construction of valid identifiers. For example, to
945+
refer to the `R` variable `draw.segments`, you can use `var"draw.segments"` in
946+
your Julia code.
947+
948+
It is also used to `show` julia source code which has gone through macro
949+
hygiene or otherwise contains variable names which can't be parsed normally.
950+
951+
Note that this syntax requires parser support so it is expanded directly by the
952+
parser rather than being implemented as a normal string macro `@var_str`.
953+
954+
!!! compat "Julia 1.3"
955+
This syntax requires at least Julia 1.3.
956+
957+
"""
958+
kw"var\"name\"", kw"@var_str"
959+
937960
"""
938961
ans
939962

base/show.jl

+61-53
Original file line numberDiff line numberDiff line change
@@ -490,48 +490,28 @@ function show_type_name(io::IO, tn::Core.TypeName)
490490
end
491491
end
492492
sym = (globfunc ? globname : tn.name)::Symbol
493-
if get(io, :compact, false)
494-
if globfunc
495-
return print(io, "typeof(", sym, ")")
496-
else
497-
return print(io, sym)
498-
end
499-
end
500-
sym_str = string(sym)
501-
hidden = !globfunc && '#' sym_str
493+
globfunc && print(io, "typeof(")
502494
quo = false
503-
if hidden
504-
print(io, "getfield(")
505-
elseif globfunc
506-
print(io, "typeof(")
507-
end
508-
# Print module prefix unless type is visible from module passed to IOContext
509-
# If :module is not set, default to Main. nothing can be used to force printing prefix
510-
from = get(io, :module, Main)
511-
if isdefined(tn, :module) && (hidden || from === nothing || !isvisible(sym, tn.module, from))
512-
show(io, tn.module)
513-
if !hidden
495+
if !get(io, :compact, false)
496+
# Print module prefix unless type is visible from module passed to
497+
# IOContext If :module is not set, default to Main. nothing can be used
498+
# to force printing prefix
499+
from = get(io, :module, Main)
500+
if isdefined(tn, :module) && (from === nothing || !isvisible(sym, tn.module, from))
501+
show(io, tn.module)
514502
print(io, ".")
515-
if globfunc && !is_id_start_char(first(sym_str))
516-
print(io, ":")
517-
if sym == :(==)
518-
print(io, "(")
503+
if globfunc && !is_id_start_char(first(string(sym)))
504+
print(io, ':')
505+
if sym in quoted_syms
506+
print(io, '(')
519507
quo = true
520508
end
521509
end
522510
end
523511
end
524-
if hidden
525-
print(io, ", Symbol(\"", sym_str, "\"))")
526-
else
527-
print(io, sym_str)
528-
if globfunc
529-
print(io, ")")
530-
if quo
531-
print(io, ")")
532-
end
533-
end
534-
end
512+
show_sym(io, sym)
513+
quo && print(io, ")")
514+
globfunc && print(io, ")")
535515
end
536516

537517
function show_datatype(io::IO, x::DataType)
@@ -814,7 +794,7 @@ julia> Base.isoperator(:+), Base.isoperator(:f)
814794
(true, false)
815795
```
816796
"""
817-
isoperator(s::Symbol) = ccall(:jl_is_operator, Cint, (Cstring,), s) != 0
797+
isoperator(s::Union{Symbol,AbstractString}) = ccall(:jl_is_operator, Cint, (Cstring,), s) != 0
818798

819799
"""
820800
isunaryoperator(s::Symbol)
@@ -981,7 +961,7 @@ end
981961
function show_call(io::IO, head, func, func_args, indent)
982962
op, cl = expr_calls[head]
983963
if (isa(func, Symbol) && func !== :(:) && !(head === :. && isoperator(func))) ||
984-
(isa(func, Expr) && (func.head == :. || func.head == :curly)) ||
964+
(isa(func, Expr) && (func.head == :. || func.head == :curly || func.head == :macroname)) ||
985965
isa(func, GlobalRef)
986966
show_unquoted(io, func, indent)
987967
else
@@ -1003,10 +983,24 @@ function show_call(io::IO, head, func, func_args, indent)
1003983
end
1004984
end
1005985

986+
# Print `sym` as it would appear as an identifier name in code
987+
# * Print valid identifiers & operators literally; also macros names if allow_macroname=true
988+
# * Escape invalid identifiers with var"" syntax
989+
function show_sym(io::IO, sym; allow_macroname=false)
990+
if isidentifier(sym) || isoperator(sym)
991+
print(io, sym)
992+
elseif allow_macroname && (sym_str = string(sym); startswith(sym_str, '@'))
993+
print(io, '@')
994+
show_sym(io, sym_str[2:end])
995+
else
996+
print(io, "var", repr(string(sym)))
997+
end
998+
end
999+
10061000
## AST printing ##
10071001

10081002
show_unquoted(io::IO, val::SSAValue, ::Int, ::Int) = print(io, "%", val.id)
1009-
show_unquoted(io::IO, sym::Symbol, ::Int, ::Int) = print(io, sym)
1003+
show_unquoted(io::IO, sym::Symbol, ::Int, ::Int) = show_sym(io, sym)
10101004
show_unquoted(io::IO, ex::LineNumberNode, ::Int, ::Int) = show_linenumber(io, ex.line, ex.file)
10111005
show_unquoted(io::IO, ex::GotoNode, ::Int, ::Int) = print(io, "goto %", ex.label)
10121006
function show_unquoted(io::IO, ex::GlobalRef, ::Int, ::Int)
@@ -1016,7 +1010,7 @@ function show_unquoted(io::IO, ex::GlobalRef, ::Int, ::Int)
10161010
parens = quoted && (!isoperator(ex.name) || (ex.name in quoted_syms))
10171011
quoted && print(io, ':')
10181012
parens && print(io, '(')
1019-
print(io, ex.name)
1013+
show_sym(io, ex.name, allow_macroname=true)
10201014
parens && print(io, ')')
10211015
nothing
10221016
end
@@ -1094,7 +1088,7 @@ end
10941088

10951089
function show_import_path(io::IO, ex)
10961090
if !isa(ex, Expr)
1097-
print(io, ex)
1091+
show_unquoted(io, ex)
10981092
elseif ex.head === :(:)
10991093
show_import_path(io, ex.args[1])
11001094
print(io, ": ")
@@ -1105,18 +1099,21 @@ function show_import_path(io::IO, ex)
11051099
show_import_path(io, ex.args[i])
11061100
end
11071101
elseif ex.head === :(.)
1108-
print(io, ex.args[1])
1109-
for i = 2:length(ex.args)
1110-
if ex.args[i-1] != :(.)
1102+
for i = 1:length(ex.args)
1103+
if i > 1 && ex.args[i-1] != :(.)
11111104
print(io, '.')
11121105
end
1113-
print(io, ex.args[i])
1106+
show_sym(io, ex.args[i], allow_macroname=(i==length(ex.args)))
11141107
end
11151108
else
11161109
show_unquoted(io, ex)
11171110
end
11181111
end
11191112

1113+
# Wrap symbols for macro names to allow them to be printed literally
1114+
allow_macroname(ex) = ex isa Symbol && first(string(ex)) == '@' ?
1115+
Expr(:macroname, ex) : ex
1116+
11201117
# TODO: implement interpolated strings
11211118
function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int)
11221119
head, args, nargs = ex.head, ex.args, length(ex.args)
@@ -1279,7 +1276,9 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int)
12791276
print(io, "end")
12801277

12811278
elseif (head === :function || head === :macro) && nargs == 1
1282-
print(io, head, ' ', args[1], " end")
1279+
print(io, head, ' ')
1280+
show_unquoted(io, args[1])
1281+
print(io, " end")
12831282

12841283
elseif head === :do && nargs == 2
12851284
show_unquoted(io, args[1], indent, -1)
@@ -1344,26 +1343,39 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int)
13441343
print(io, head)
13451344

13461345
elseif (nargs == 1 && head in (:return, :const)) ||
1347-
head in (:local, :global, :export)
1346+
head in (:local, :global)
13481347
print(io, head, ' ')
13491348
show_list(io, args, ", ", indent)
13501349

1350+
elseif head === :export
1351+
print(io, head, ' ')
1352+
show_list(io, allow_macroname.(args), ", ", indent)
1353+
13511354
elseif head === :macrocall && nargs >= 2
13521355
# first show the line number argument as a comment
13531356
if isa(args[2], LineNumberNode) || is_expr(args[2], :line)
13541357
print(io, args[2], ' ')
13551358
end
13561359
# Use the functional syntax unless specifically designated with prec=-1
13571360
# and hide the line number argument from the argument list
1361+
mname = allow_macroname(args[1])
13581362
if prec >= 0
1359-
show_call(io, :call, args[1], args[3:end], indent)
1363+
show_call(io, :call, mname, args[3:end], indent)
13601364
else
13611365
show_args = Vector{Any}(undef, nargs - 1)
1362-
show_args[1] = args[1]
1366+
show_args[1] = mname
13631367
show_args[2:end] = args[3:end]
13641368
show_list(io, show_args, ' ', indent)
13651369
end
13661370

1371+
elseif head === :macroname && nargs == 1
1372+
arg1 = args[1]
1373+
if arg1 isa Symbol
1374+
show_sym(io, arg1, allow_macroname=true)
1375+
else
1376+
show_unquoted(io, arg1)
1377+
end
1378+
13671379
elseif head === :line && 1 <= nargs <= 2
13681380
show_linenumber(io, args...)
13691381

@@ -1406,11 +1418,7 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int)
14061418
for x in args
14071419
if !isa(x,AbstractString)
14081420
print(io, "\$(")
1409-
if isa(x,Symbol) && !(x in quoted_syms)
1410-
print(io, x)
1411-
else
1412-
show_unquoted(io, x)
1413-
end
1421+
show_unquoted(io, x)
14141422
print(io, ")")
14151423
else
14161424
escape_string(io, x, "\"\$")

doc/src/base/base.md

+1
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ Base.@nospecialize
251251
Base.@specialize
252252
Base.gensym
253253
Base.@gensym
254+
var"name"
254255
Base.@goto
255256
Base.@label
256257
Base.@simd

src/julia-parser.scm

+19-7
Original file line numberDiff line numberDiff line change
@@ -1068,10 +1068,11 @@
10681068
;; -2^3 is parsed as -(2^3), so call parse-decl for the first argument,
10691069
;; and parse-unary from then on (to handle 2^-3)
10701070
(define (parse-factor s)
1071-
(parse-factor-with-initial-ex s (parse-unary-prefix s)))
1071+
(let ((nxt (peek-token s)))
1072+
(parse-factor-with-initial-ex s (parse-unary-prefix s) nxt)))
10721073

1073-
(define (parse-factor-with-initial-ex s ex0)
1074-
(let* ((ex (parse-decl-with-initial-ex s (parse-call-with-initial-ex s ex0)))
1074+
(define (parse-factor-with-initial-ex s ex0 (tok #f))
1075+
(let* ((ex (parse-decl-with-initial-ex s (parse-call-with-initial-ex s ex0 tok)))
10751076
(t (peek-token s)))
10761077
(if (is-prec-power? t)
10771078
(begin (take-token s)
@@ -1100,10 +1101,11 @@
11001101
;; parse function call, indexing, dot, and transpose expressions
11011102
;; also handles looking for syntactic reserved words
11021103
(define (parse-call s)
1103-
(parse-call-with-initial-ex s (parse-unary-prefix s)))
1104+
(let ((nxt (peek-token s)))
1105+
(parse-call-with-initial-ex s (parse-unary-prefix s) nxt)))
11041106

1105-
(define (parse-call-with-initial-ex s ex)
1106-
(if (or (initial-reserved-word? ex) (eq? ex 'mutable) (eq? ex 'primitive) (eq? ex 'abstract))
1107+
(define (parse-call-with-initial-ex s ex tok)
1108+
(if (or (initial-reserved-word? tok) (memq tok '(mutable primitive abstract)))
11071109
(parse-resword s ex)
11081110
(parse-call-chain s ex #f)))
11091111

@@ -2282,7 +2284,17 @@
22822284
(begin (check-identifier t)
22832285
(if (closing-token? t)
22842286
(error (string "unexpected \"" (take-token s) "\"")))))
2285-
(take-token s))
2287+
(take-token s)
2288+
(if (and (eq? t 'var) (eqv? (peek-token s) #\") (not (ts:space? s)))
2289+
(begin
2290+
;; var"funky identifier" syntax
2291+
(take-token s)
2292+
(let ((str (parse-raw-literal s #\"))
2293+
(nxt (peek-token s)))
2294+
(if (and (symbol? nxt) (not (operator? nxt)) (not (ts:space? s)))
2295+
(error (string "suffix not allowed after `var\"" str "\"`")))
2296+
(symbol str)))
2297+
t))
22862298

22872299
;; parens or tuple
22882300
((eqv? t #\( )

stdlib/Test/src/Test.jl

+5-12
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ function eval_test(evaluated::Expr, quoted::Expr, source::LineNumberNode, negate
236236
evaled_args = evaluated.args
237237
quoted_args = quoted.args
238238
n = length(evaled_args)
239+
kw_suffix = ""
239240
if evaluated.head == :comparison
240241
args = evaled_args
241242
while i < n
@@ -260,17 +261,9 @@ function eval_test(evaluated::Expr, quoted::Expr, source::LineNumberNode, negate
260261
func_sym = quoted_args[1]
261262
if isempty(kwargs)
262263
quoted = Expr(:call, func_sym, args...)
263-
elseif func_sym === :
264-
# in case of `≈(x, y, atol = z)`
265-
# make the display like `Evaluated: x ≈ y (atol=z)`
266-
kws = [Symbol(Expr(:kw, k, v), ",") for (k, v) in kwargs]
267-
kws[end] = Symbol(Expr(:kw, kwargs[end]...))
268-
kws[1] = Symbol("(", kws[1])
269-
kws[end] = Symbol(kws[end], ")")
270-
quoted = Expr(:comparison, args[1], func_sym, args[2], kws...)
271-
if length(quoted.args) & 1 == 0 # hack to fit `show_unquoted`
272-
push!(quoted.args, Symbol())
273-
end
264+
elseif func_sym === : && !res
265+
quoted = Expr(:call, func_sym, args...)
266+
kw_suffix = " ($(join(["$k=$v" for (k, v) in kwargs], ", ")))"
274267
else
275268
kwargs_expr = Expr(:parameters, [Expr(:kw, k, v) for (k, v) in kwargs]...)
276269
quoted = Expr(:call, func_sym, kwargs_expr, args...)
@@ -286,7 +279,7 @@ function eval_test(evaluated::Expr, quoted::Expr, source::LineNumberNode, negate
286279

287280
Returned(res,
288281
# stringify arguments in case of failure, for easy remote printing
289-
res ? quoted : sprint(io->print(IOContext(io, :limit => true), quoted)),
282+
res ? quoted : sprint(io->print(IOContext(io, :limit => true), quoted))*kw_suffix,
290283
source)
291284
end
292285

test/show.jl

+19-3
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,22 @@ end
351351
@test sprint(show, :+) == ":+"
352352
@test sprint(show, :end) == ":end"
353353

354+
# issue #32408: Printing of names which are invalid identifiers
355+
# Invalid identifiers which need `var` quoting:
356+
@test sprint(show, Expr(:call, :foo, Symbol("##"))) == ":(foo(var\"##\"))"
357+
@test sprint(show, Expr(:call, :foo, Symbol("a-b"))) == ":(foo(var\"a-b\"))"
358+
@test sprint(show, :(export var"#")) == ":(export var\"#\")"
359+
@test sprint(show, :(import A: var"#")) == ":(import A: var\"#\")"
360+
@test sprint(show, :(macro var"#" end)) == ":(macro var\"#\" end)"
361+
@test sprint(show, :"x$(var"#")y") == ":(\"x\$(var\"#\")y\")"
362+
# Macro-like names outside macro calls
363+
@test sprint(show, Expr(:call, :foo, Symbol("@bar"))) == ":(foo(var\"@bar\"))"
364+
@test sprint(show, :(export @foo)) == ":(export @foo)"
365+
@test sprint(show, :(import A.B: c.@d)) == ":(import A.B: c.@d)"
366+
@test sprint(show, :(using A.@foo)) == ":(using A.@foo)"
367+
# Hidden macro names
368+
@test sprint(show, Expr(:macrocall, Symbol("@#"), nothing, :a)) == ":(@var\"#\" a)"
369+
354370
# issue #12477
355371
@test sprint(show, Union{Int64, Int32, Int16, Int8, Float64}) == "Union{Float64, Int16, Int32, Int64, Int8}"
356372

@@ -580,9 +596,9 @@ function f13127()
580596
show(buf, f)
581597
String(take!(buf))
582598
end
583-
@test startswith(f13127(), "getfield($(@__MODULE__), Symbol(\"")
599+
@test startswith(f13127(), "$(@__MODULE__).var\"#f")
584600

585-
@test startswith(sprint(show, typeof(x->x), context = :module=>@__MODULE__), "getfield($(@__MODULE__), Symbol(\"")
601+
@test startswith(sprint(show, typeof(x->x), context = :module=>@__MODULE__), "var\"")
586602

587603
#test methodshow.jl functions
588604
@test Base.inbase(Base)
@@ -1146,7 +1162,7 @@ end
11461162
@test repr(typeof(UnexportedOperators.:(==))) == "typeof($(curmod_prefix)UnexportedOperators.:(==))"
11471163
anonfn = x->2x
11481164
modname = string(@__MODULE__)
1149-
anonfn_type_repr = "getfield($modname, Symbol(\"$(typeof(anonfn).name.name)\"))"
1165+
anonfn_type_repr = "$modname.var\"$(typeof(anonfn).name.name)\""
11501166
@test repr(typeof(anonfn)) == anonfn_type_repr
11511167
@test repr(anonfn) == anonfn_type_repr * "()"
11521168
@test repr("text/plain", anonfn) == "$(typeof(anonfn).name.mt.name) (generic function with 1 method)"

0 commit comments

Comments
 (0)