Skip to content

Commit a32a066

Browse files
fonspstevengj
andauthored
[REPL] More accurate ends_with_semicolon (#43374)
* More accurate `ends_with_semicolon` Co-authored-by: Steven G. Johnson <stevenj@mit.edu>
1 parent 39c7355 commit a32a066

File tree

3 files changed

+53
-47
lines changed

3 files changed

+53
-47
lines changed

doc/src/manual/performance-tips.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1024,7 +1024,7 @@ consider the two functions:
10241024
```jldoctest dotfuse
10251025
julia> f(x) = 3x.^2 + 4x + 7x.^3;
10261026
1027-
julia> fdot(x) = @. 3x^2 + 4x + 7x^3 # equivalent to 3 .* x.^2 .+ 4 .* x .+ 7 .* x.^3;
1027+
julia> fdot(x) = @. 3x^2 + 4x + 7x^3; # equivalent to 3 .* x.^2 .+ 4 .* x .+ 7 .* x.^3
10281028
```
10291029

10301030
Both `f` and `fdot` compute the same thing. However, `fdot`

stdlib/REPL/src/REPL.jl

Lines changed: 36 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,57 +1273,50 @@ answer_color(r::StreamREPL) = r.answer_color
12731273
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
12741274
input_color(r::StreamREPL) = r.input_color
12751275

1276-
# heuristic function to decide if the presence of a semicolon
1277-
# at the end of the expression was intended for suppressing output
1278-
function ends_with_semicolon(line::AbstractString)
1279-
match = findlast(isequal(';'), line)::Union{Nothing,Int}
1280-
if match !== nothing
1281-
# state for comment parser, assuming that the `;` isn't in a string or comment
1282-
# so input like ";#" will still thwart this to give the wrong (anti-conservative) answer
1283-
comment = false
1284-
comment_start = false
1285-
comment_close = false
1286-
comment_multi = 0
1287-
for c in line[(match + 1):end]
1288-
if comment_multi > 0
1289-
# handle nested multi-line comments
1290-
if comment_close && c == '#'
1291-
comment_close = false
1292-
comment_multi -= 1
1293-
elseif comment_start && c == '='
1294-
comment_start = false
1295-
comment_multi += 1
1296-
else
1297-
comment_start = (c == '#')
1298-
comment_close = (c == '=')
1299-
end
1300-
elseif comment
1301-
# handle line comments
1302-
if c == '\r' || c == '\n'
1303-
comment = false
1276+
let matchend = Dict("\"" => r"\"", "\"\"\"" => r"\"\"\"", "'" => r"'",
1277+
"`" => r"`", "```" => r"```", "#" => r"$"m, "#=" => r"=#|#=")
1278+
global _rm_strings_and_comments
1279+
function _rm_strings_and_comments(code::Union{String,SubString{String}})
1280+
buf = IOBuffer(sizehint = sizeof(code))
1281+
pos = 1
1282+
while true
1283+
i = findnext(r"\"(?!\"\")|\"\"\"|'|`(?!``)|```|#(?!=)|#=", code, pos)
1284+
isnothing(i) && break
1285+
match = SubString(code, i)
1286+
j = findnext(matchend[match]::Regex, code, nextind(code, last(i)))
1287+
if match == "#=" # possibly nested
1288+
nested = 1
1289+
while j !== nothing
1290+
nested += SubString(code, j) == "#=" ? +1 : -1
1291+
iszero(nested) && break
1292+
j = findnext(r"=#|#=", code, nextind(code, last(j)))
13041293
end
1305-
elseif comment_start
1306-
# see what kind of comment this is
1307-
comment_start = false
1308-
if c == '='
1309-
comment_multi = 1
1310-
else
1311-
comment = true
1294+
elseif match[1] != '#' # quote match: check non-escaped
1295+
while j !== nothing
1296+
notbackslash = findprev(!=('\\'), code, prevind(code, first(j)))::Int
1297+
isodd(first(j) - notbackslash) && break # not escaped
1298+
j = findnext(matchend[match]::Regex, code, nextind(code, first(j)))
13121299
end
1313-
elseif c == '#'
1314-
# start handling for a comment
1315-
comment_start = true
1300+
end
1301+
isnothing(j) && break
1302+
if match[1] == '#'
1303+
print(buf, SubString(code, pos, prevind(code, first(i))))
13161304
else
1317-
# outside of a comment, encountering anything but whitespace
1318-
# means the semi-colon was internal to the expression
1319-
isspace(c) || return false
1305+
print(buf, SubString(code, pos, last(i)), ' ', SubString(code, j))
13201306
end
1307+
pos = nextind(code, last(j))
13211308
end
1322-
return true
1309+
print(buf, SubString(code, pos, lastindex(code)))
1310+
return String(take!(buf))
13231311
end
1324-
return false
13251312
end
13261313

1314+
# heuristic function to decide if the presence of a semicolon
1315+
# at the end of the expression was intended for suppressing output
1316+
ends_with_semicolon(code::AbstractString) = ends_with_semicolon(String(code))
1317+
ends_with_semicolon(code::Union{String,SubString{String}}) =
1318+
contains(_rm_strings_and_comments(code), r";\s*$")
1319+
13271320
function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
13281321
have_color = hascolor(repl)
13291322
Base.banner(repl.stream)

stdlib/REPL/test/repl.jl

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -913,16 +913,29 @@ end
913913
let ends_with_semicolon = REPL.ends_with_semicolon
914914
@test !ends_with_semicolon("")
915915
@test ends_with_semicolon(";")
916-
@test !ends_with_semicolon("a")
916+
@test !ends_with_semicolon("ä")
917+
@test !ends_with_semicolon("ä # äsdf ;")
918+
@test ends_with_semicolon("""a * "#ä" ;""")
919+
@test ends_with_semicolon("a; #=#=# =# =#\n")
917920
@test ends_with_semicolon("1;")
918921
@test ends_with_semicolon("1;\n")
919922
@test ends_with_semicolon("1;\r")
920923
@test ends_with_semicolon("1;\r\n \t\f")
921-
@test ends_with_semicolon("1;#text\n")
922-
@test ends_with_semicolon("a; #=#=# =# =#\n")
924+
@test ends_with_semicolon("1;#äsdf\n")
925+
@test ends_with_semicolon("""1;\n#äsdf\n""")
926+
@test !ends_with_semicolon("\"\\\";\"#\"")
927+
@test ends_with_semicolon("\"\\\\\";#\"")
923928
@test !ends_with_semicolon("begin\na;\nb;\nend")
924929
@test !ends_with_semicolon("begin\na; #=#=#\n=#b=#\nend")
925930
@test ends_with_semicolon("\na; #=#=#\n=#b=#\n# test\n#=\nfoobar\n=##bazbax\n")
931+
@test ends_with_semicolon("f()= 1; # é ; 2")
932+
@test ends_with_semicolon("f()= 1; # é")
933+
@test !ends_with_semicolon("f()= 1; \"é\"")
934+
@test !ends_with_semicolon("""("f()= 1; # é")""")
935+
@test !ends_with_semicolon(""" "f()= 1; # é" """)
936+
@test ends_with_semicolon("f()= 1;")
937+
# the next result does not matter because this is not legal syntax
938+
@test_nowarn ends_with_semicolon("1; #=# 2")
926939
end
927940

928941
# PR #20794, TTYTerminal with other kinds of streams

0 commit comments

Comments
 (0)