Browse Source

Make Runic compilable with juliac (#56)

pull/57/head
Fredrik Ekre 1 year ago committed by GitHub
parent
commit
5c017e5524
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      Project.toml
  2. 1
      juliac/.gitignore
  3. 26
      juliac/Makefile
  4. 11
      juliac/runicc.jl
  5. 32
      src/Runic.jl
  6. 15
      src/chisels.jl
  7. 203
      src/juliac.jl
  8. 253
      src/main.jl
  9. 5
      src/runestone.jl

2
Project.toml

@ -6,7 +6,7 @@ version = "1.0.0"
JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4"
[compat] [compat]
JuliaSyntax = "0.4.8" JuliaSyntax = "0.4.10"
[extras] [extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

1
juliac/.gitignore vendored

@ -0,0 +1 @@
/runicc

26
juliac/Makefile

@ -0,0 +1,26 @@
JULIA ?= /opt/julia/julia-c/bin/julia
JULIAC ?= $(shell $(JULIA) -e 'print(normpath(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia", "juliac.jl")))')
RUNIC_SRCFILES := $(wildcard ../src/*.jl)
runicc: runicc.jl $(RUNIC_SRCFILES) invalidate-precompile-cache
$(JULIA) $(JULIAC) --output-exe $@ --trim=unsafe-warn $<
clean:
rm runicc
# Prune cached precompile files for Runic. This is needed because there are
# (compile time) branches in the Runic source code which depends on whether
# Runic is compiled or not. It looks like juliac will use existing cache files
# but not produce any so there is no need to prune them again after compilation
# to force regular usage to recompile.
invalidate-precompile-cache:
$(JULIA) -e ' \
ji = Base.compilecache_path(Base.PkgId(Base.UUID("62bfec6d-59d7-401d-8490-b29ee721c001"), "Runic")); \
if ji !== nothing; \
isfile(ji) && (@info "Deleting precompile file $$(ji)"; rm(ji)); \
so = splitext(ji)[1] * "." * Base.BinaryPlatforms.platform_dlext(); \
isfile(so) && (@info "Deleting pkgimage file $$(so)"; rm(so)); \
end'
.PHONY: invalidate-precompile-cache clean

11
juliac/runicc.jl

@ -0,0 +1,11 @@
module RunicC
using Runic: Runic
# TODO: Why do we need this shim? Wouldn't it be possible to just compile `src/Runic.jl`?
Base.@ccallable function main()::Cint
argv = String[]
return Runic.main(argv)
end
end

32
src/Runic.jl

@ -51,10 +51,18 @@ end
# Re-package a GreenNode as a Node # Re-package a GreenNode as a Node
function Node(node::JuliaSyntax.GreenNode) function Node(node::JuliaSyntax.GreenNode)
tags = 0 % TagType tags = 0 % TagType
return Node( # juliac: this is `kids = map(Node, JuliaSyntax.children(node))` but written out like
JuliaSyntax.head(node), JuliaSyntax.span(node), # this in order to help inference.
map(Node, JuliaSyntax.children(node)), tags children = JuliaSyntax.children(node)
) if children isa Tuple{}
kids = ()
else
kids = Vector{Node}(undef, length(children))
for (i, child) in pairs(children)
kids[i] = Node(child)
end
end
return Node(JuliaSyntax.head(node), JuliaSyntax.span(node), kids, tags)
end end
function Base.show(io::IO, ::MIME"text/plain", node::Node) function Base.show(io::IO, ::MIME"text/plain", node::Node)
@ -235,11 +243,11 @@ function check_format_toggle(ctx::Context, node::Node, kid::Node, i::Int)::Union
offmatch === nothing && return nothing offmatch === nothing && return nothing
toggle = offmatch.captures[3]::AbstractString toggle = offmatch.captures[3]::AbstractString
if toggle == "on" if toggle == "on"
@debug "Ignoring `$(offmatch.match)` toggle since formatting is already on." # @debug "Ignoring `$(offmatch.match)` toggle since formatting is already on."
return nothing return nothing
end end
if !validate_toggle(ctx, kids, i) if !validate_toggle(ctx, kids, i)
@debug "Ignoring `$(offmatch.match)` toggle since it is not on a separate line." # @debug "Ignoring `$(offmatch.match)` toggle since it is not on a separate line."
return nothing return nothing
end end
# Find a matching closing toggle # Find a matching closing toggle
@ -260,20 +268,20 @@ function check_format_toggle(ctx::Context, node::Node, kid::Node, i::Int)::Union
# Check that the comments match in style # Check that the comments match in style
if offmatch.captures[1] != onmatch.captures[1] || if offmatch.captures[1] != onmatch.captures[1] ||
offmatch.captures[2] != onmatch.captures[2] offmatch.captures[2] != onmatch.captures[2]
@debug "Ignoring `$(onmatch.match)` toggle since it doesn't match the " * # @debug "Ignoring `$(onmatch.match)` toggle since it doesn't match the " *
"style of the `$(offmatch.match)` toggle." # "style of the `$(offmatch.match)` toggle."
accept_node!(ctx, lkid) accept_node!(ctx, lkid)
continue continue
end end
toggle = onmatch.captures[3]::AbstractString toggle = onmatch.captures[3]::AbstractString
if toggle == "off" if toggle == "off"
@debug "Ignoring `$(onmatch.match)` toggle since formatting is already off." # @debug "Ignoring `$(onmatch.match)` toggle since formatting is already off."
accept_node!(ctx, lkid) accept_node!(ctx, lkid)
continue continue
end end
@assert toggle == "on" @assert toggle == "on"
if !validate_toggle(ctx, kids, j) if !validate_toggle(ctx, kids, j)
@debug "Ignoring `$(onmatch.match)` toggle since it is not on a separate line." # @debug "Ignoring `$(onmatch.match)` toggle since it is not on a separate line."
accept_node!(ctx, lkid) accept_node!(ctx, lkid)
continue continue
end end
@ -287,8 +295,8 @@ function check_format_toggle(ctx::Context, node::Node, kid::Node, i::Int)::Union
if length(ctx.lineage_kinds) == 1 && ctx.lineage_kinds[1] === K"toplevel" if length(ctx.lineage_kinds) == 1 && ctx.lineage_kinds[1] === K"toplevel"
return typemax(Int) return typemax(Int)
end end
@debug "Ignoring `$(offmatch.match)` toggle since no matching `on` toggle " * # @debug "Ignoring `$(offmatch.match)` toggle since no matching `on` toggle " *
"was found at the same tree level." # "was found at the same tree level."
return nothing return nothing
end end

15
src/chisels.jl

@ -6,8 +6,19 @@
# JuliaSyntax.jl overloads == for this but seems easier to just define a new function # JuliaSyntax.jl overloads == for this but seems easier to just define a new function
function nodes_equal(n1::Node, n2::Node) function nodes_equal(n1::Node, n2::Node)
return head(n1) == head(n2) && span(n1) == span(n2) && # n1.tags == n2.tags && head(n1) == head(n2) && span(n1) == span(n2) || return false
all(((x, y),) -> nodes_equal(x, y), zip(n1.kids, n2.kids)) # juliac: this is `all(((x, y),) -> nodes_equal(x, y), zip(n1.kids, n2.kids))` but
# written out as an explicit loop to help inference.
if is_leaf(n1)
return is_leaf(n2)
end
kids1 = verified_kids(n1)
kids2 = verified_kids(n2)
length(kids1) == length(kids2) || return false
for i in eachindex(kids1)
nodes_equal(n1.kids[i], n2.kids[i]) || return false
end
return true
end end
# See JuliaSyntax/src/parse_stream.jl # See JuliaSyntax/src/parse_stream.jl

203
src/juliac.jl

@ -0,0 +1,203 @@
# SPDX-License-Identifier: MIT
# juliac-compatible replacement for `read(stdin)`
function read_juliac()
bytes = UInt8[]
size = 1
nmemb = 1024
buf = zeros(UInt8, nmemb)
file = Libc.FILE(RawFD(0), "r") # FILE constructor calls `fdopen`
local fread, feof, ferror # Silence of the Langs(erver)
while true
nread = @ccall fread(buf::Ptr{UInt8}, size::Csize_t, nmemb::Cint, file::Ptr{Libc.FILE})::Csize_t
append!(bytes, @view(buf[1:nread]))
if nread < nmemb
if (@ccall feof(file::Ptr{Libc.FILE})::Cint) != 0
close(file)
break
else
@assert (@ccall ferror(file::Ptr{Libc.FILE})::Cint) != 0
close(file)
error("ferror: fread failed")
end
end
end
return bytes
end
# juliac-compatible `Base.printstyled` that simply forces color
# TODO: detect color support (and maybe support `--color=(yes|no)`?), right now color is
# forced. For juliac we can detect whether stdout/stderr is a tty with
# `(@ccall isatty(RawFD(1)::Cint)::Cint) == 1`.
function printstyled_juliac(io::IO, str::String; bold = false, color::Symbol = :normal)
@assert io === Core.stdout || io === Core.stderr
@assert !occursin('\n', str)
color === :red && write(io, "\e[31m")
color === :green && write(io, "\e[32m")
color === :blue && write(io, "\e[34m")
color === :normal && write(io, "\e[0m")
bold && write(io, "\e[1m")
print(io, str)
bold && write(io, "\e[22m")
color in (:red, :green, :blue) && write(io, "\e[39m")
return
end
# juliac-compatible `Base.showerror`
function sprint_showerror_juliac(err::Exception)
if err isa SystemError
return "SystemError: " * err.prefix * ": " * Libc.strerror(err.errnum)
elseif err isa AssertionError
# sprint uses dynamic dispatch
io = IOBuffer()
showerror(io, err)
return String(take!(io))
else
return string(typeof(err))
end
end
# juliac-compatible `Base.tempdir` and `Base.mktempdir` without logging and deferred cleanup
function tempdir_juliac()
buf = Base.StringVector(Base.Filesystem.AVG_PATH - 1)
sz = Base.RefValue{Csize_t}(length(buf) + 1)
while true
rc = ccall(:uv_os_tmpdir, Cint, (Ptr{UInt8}, Ptr{Csize_t}), buf, sz)
if rc == 0
resize!(buf, sz[])
break
elseif rc == Base.UV_ENOBUFS
resize!(buf, sz[] - 1)
else
Base.uv_error("tempdir()", rc)
end
end
tempdir = String(buf)
return tempdir
end
function mktempdir_juliac()
parent = tempdir_juliac()
prefix = Base.Filesystem.temp_prefix
if isempty(parent) || occursin(Base.Filesystem.path_separator_re, parent[end:end])
tpath = "$(parent)$(prefix)XXXXXX"
else
tpath = "$(parent)$(Base.Filesystem.path_separator)$(prefix)XXXXXX"
end
req = Libc.malloc(Base._sizeof_uv_fs)
try
ret = ccall(
:uv_fs_mkdtemp, Cint,
(Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Ptr{Cvoid}),
C_NULL, req, tpath, C_NULL
)
if ret < 0
Base.Filesystem.uv_fs_req_cleanup(req)
Base.uv_error("mktempdir($(repr(parent)))", ret)
end
path = unsafe_string(ccall(:jl_uv_fs_t_path, Cstring, (Ptr{Cvoid},), req))
Base.Filesystem.uv_fs_req_cleanup(req)
return path
finally
Libc.free(req)
end
end
function mktempdir_juliac(f::F) where {F}
tmpdir = mktempdir_juliac()
try
f(tmpdir)
finally
try
rm(tmpdir; force = true, recursive = true)
catch
end
end
return
end
# juliac-compatible `run(::Base.CmdRedirect)` where both stdout and stderr are redirected
# and read.
function run_juliac(cmd::Base.CmdRedirect)
# Unpack the redirection layers
@assert cmd.stream_no == 2
@assert cmd.handle::Core.CoreSTDERR === Core.stderr
cmd′ = cmd.cmd::Base.CmdRedirect
@assert cmd′.stream_no == 1
@assert cmd′.handle::Core.CoreSTDERR === Core.stderr
cmd′′ = cmd′.cmd::Cmd
@assert cmd′′.ignorestatus
argv = cmd′′.exec
dir = cmd′′.dir
# Run the command
bytes = pipe_fork_exec(argv, dir)
# Write output
write(Core.stderr, bytes)
return
end
function WIFEXITED(status)
return (status[] & 0x7f) == 0
end
function WEXITSTATUS(status)
return (status[] & 0xff00) >> 8
end
function pipe_fork_exec(argv::Vector{String}, dir::String)
local pipe, fork, dup2, chdir, execv, waitpid # Silence of the Langs(erver)
# Set up the pipe
fds = Vector{Cint}(undef, 2)
READ_END, WRITE_END = 1, 2
err = @ccall pipe(fds::Ref{Cint})::Cint
err == -1 && systemerror("pipe")
# Fork
cpid = @ccall fork()::Cint
cpid == -1 && systemerror("fork")
# Handle the child process
if cpid == 0
# Close read end of the pipe
err = @ccall close(fds[READ_END]::Cint)::Cint
err == -1 && systemerror("close")
# Duplicate write end of the pipe to stdout and stderr
STDOUT_FILENO, STDERR_FILENO = 1, 2
err = @ccall dup2(fds[WRITE_END]::Cint, STDOUT_FILENO::Cint)::Cint
err == -1 && systemerror("dup2")
err = @ccall dup2(fds[WRITE_END]::Cint, STDERR_FILENO::Cint)::Cint
err = @ccall close(fds[WRITE_END]::Cint)::Cint # No longer needed
err == -1 && systemerror("close")
# Change directory
err = @ccall chdir(dir::Cstring)::Cint
err == 0 || systemerror("chdir")
# Execute the command
@ccall execv(argv[1]::Cstring, argv::Ref{Cstring})::Cint
systemerror("execv")
end
# Continuing the parent process
# Close write end of the pipe
err = @ccall close(fds[WRITE_END]::Cint)::Cint
err == -1 && systemerror("close")
bytes = UInt8[]
buf = Vector{UInt8}(undef, 1024)
while true
nread = @ccall read(fds[READ_END]::Cint, buf::Ptr{Cvoid}, 1024::Csize_t)::Cssize_t
nread == -1 && systemerror("read")
nread == 0 && break # eof
append!(bytes, @view(buf[1:nread]))
end
err = @ccall close(fds[READ_END]::Cint)::Cint # Close the read end of the pipe
err == -1 && systemerror("close")
# Check exit status of the child
status = Ref{Cint}()
wpid = @ccall waitpid(cpid::Cint, status::Ref{Cint}, 0::Cint)::Cint
wpid == -1 && systemerror("waitpid")
if !WIFEXITED(status)
error("child process did not exit normally")
end
# crc = WEXITSTATUS(status) # ignore this like `ignorestatus(cmd)`
return bytes
end

253
src/main.jl

@ -6,42 +6,124 @@ else
errno = 0 errno = 0
end end
function panic(msg...) # Check whether we are compiling with juliac
printstyled(stderr, "ERROR: "; color = :red, bold = true) # TODO: I thought juliac would never use existing pkgimages but looks like it does so this
for m in msg # isn't reliable... Existing cache files are pruned in the Makefile for now.
if m isa Exception const juliac = let opts = Base.JLOptions()
showerror(stderr, m) hasfield(typeof(opts), :trim) &&
elseif m isa Vector{Base.StackFrame} getfield(opts, :trim) != 0 &&
Base.show_backtrace(stderr, m) Base.generating_output()
end
@static if juliac
stderr() = Core.stderr
stdout() = Core.stdout
include("juliac.jl")
const run_cmd = run_juliac
read_stdin(::Type{String}) = String(read_juliac())
const printstyled = printstyled_juliac
const mktempdir = mktempdir_juliac
const sprint_showerror = sprint_showerror_juliac
else
stderr() = Base.stderr
stdout() = Base.stdout
const run_cmd = Base.run
read_stdin(::Type{String}) = read(stdin, String)
const printstyled = Base.printstyled
const mktempdir = Base.mktempdir
sprint_showerror(err::Exception) = sprint(showerror, err)
end
# juliac-compatible `Base.walkdir` but since we are collecting the files eagerly anyway we
# might as well use the same method even when not compiling with juliac.
function tryf(f::F, arg, default) where {F}
try
return f(arg)
catch
return default
end
end
function scandir!(files, root)
# Don't recurse into `.git`. If e.g. a branch name ends with `.jl` there are files
# inside of `.git` which has the `.jl` extension, but they are not Julia source files.
if occursin(".git", root) && ".git" in splitpath(root)
@assert endswith(root, ".git")
return
end
tryf(isdir, root, false) || return
dirs = Vector{String}()
for f in tryf(readdir, root, String[])
jf = joinpath(root, f)
if tryf(isdir, jf, false)
push!(dirs, f)
elseif (tryf(isfile, jf, false) || tryf(islink, jf, false)) && endswith(jf, ".jl")
push!(files, jf)
else else
print(stderr, msg...) # Ignore it I guess...
end end
end end
println(stderr) for dir in dirs
scandir!(files, joinpath(root, dir))
end
return
end
function panic(
msg::String, err::Union{Exception, Nothing} = nothing,
bt::Union{Vector{Base.StackFrame}, Nothing} = nothing
)
io = stderr()
printstyled(io, "ERROR: "; color = :red, bold = true)
print(io, msg)
if err !== nothing
print(io, sprint_showerror(err))
end
@static if juliac
@assert bt === nothing
else
if bt !== nothing
Base.show_backtrace(io, bt)
end
end
println(io)
global errno = 1 global errno = 1
return errno return errno
end end
okln() = printstyled(stderr, "\n"; color = :green, bold = true) function okln()
errln() = printstyled(stderr, "\n"; color = :red, bold = true) printstyled(stderr(), ""; color = :green, bold = true)
println(stderr())
return
end
function errln()
printstyled(stderr(), ""; color = :red, bold = true)
println(stderr())
return
end
# Print a typical cli program help message # Print a typical cli program help message
function print_help() function print_help()
io = stdout io = stdout()
printstyled(io, "NAME\n", bold = true) printstyled(io, "NAME", bold = true)
println(io)
println(io, " Runic.main - format Julia source code") println(io, " Runic.main - format Julia source code")
println(io) println(io)
printstyled(io, "SYNOPSIS\n", bold = true) printstyled(io, "SYNOPSIS", bold = true)
println(io)
println(io, " Runic.main - format Julia source code")
println(io, " julia -m Runic [<options>] <path>...") println(io, " julia -m Runic [<options>] <path>...")
println(io) println(io)
printstyled(io, "DESCRIPTION\n", bold = true) printstyled(io, "DESCRIPTION", bold = true)
println(io)
println( println(
io, """ io, """
`Runic.main` (typically invoked as `julia -m Runic`) formats Julia source `Runic.main` (typically invoked as `julia -m Runic`) formats Julia source
code using the Runic.jl formatter. code using the Runic.jl formatter.
""" """
) )
printstyled(io, "OPTIONS\n", bold = true) printstyled(io, "OPTIONS", bold = true)
println(io)
println( println(
io, """ io, """
<path>... <path>...
@ -73,23 +155,39 @@ end
function maybe_expand_directory!(outfiles, dir) function maybe_expand_directory!(outfiles, dir)
if !isdir(dir) if !isdir(dir)
# Assumed a file, checked when using it # Assumed to be a file, checked when using it
push!(outfiles, dir) push!(outfiles, dir)
else
scandir!(outfiles, dir)
end
return return
end end
for (root, _, files) in walkdir(dir; onerror = (err) -> nothing)
# Don't recurse into `.git`. If e.g. a branch name ends with `.jl` there are files # juliac: type-stable output struct (required for juliac but useful in general too)
# inside of `.git` which has the `.jl` extension, but they are not Julia source struct Output{IO}
# files. which::Symbol
if occursin(".git", root) && ".git" in splitpath(root) file::String
continue stream::IO
output_is_file::Bool
output_is_samefile::Bool
end end
for file in files
if endswith(file, ".jl") function writeo(output::Output, iob)
push!(outfiles, joinpath(root, file)) @assert output.which !== :devnull
if output.which === :file
# juliac: `open(...) do` uses dynamic dispatch
# write(output.file, iob)
let io = open(output.file, "w")
try
write(io, iob)
finally
close(io)
end end
end end
elseif output.which == :stdout
write(output.stream, iob)
end end
return
end end
function main(argv) function main(argv)
@ -98,7 +196,7 @@ function main(argv)
# Default values # Default values
inputfiles = String[] inputfiles = String[]
outputfile = nothing outputfile = ""
quiet = false quiet = false
verbose = false verbose = false
debug = false debug = false
@ -153,16 +251,16 @@ function main(argv)
if inplace && check if inplace && check
return panic("options `--inplace` and `--check` are mutually exclusive") return panic("options `--inplace` and `--check` are mutually exclusive")
end end
if inplace && outputfile !== nothing if inplace && outputfile != ""
return panic("options `--inplace` and `--output` are mutually exclusive") return panic("options `--inplace` and `--output` are mutually exclusive")
end end
if check && outputfile !== nothing if check && outputfile != ""
return panic("options `--check` and `--output` are mutually exclusive") return panic("options `--check` and `--output` are mutually exclusive")
end end
if inplace && input_is_stdin if inplace && input_is_stdin
return panic("option `--inplace` can not be used together with stdin input") return panic("option `--inplace` can not be used together with stdin input")
end end
if outputfile !== nothing && length(inputfiles) > 1 if outputfile != "" && length(inputfiles) > 1
return panic("option `--output` can not be used together with multiple input files") return panic("option `--output` can not be used together with multiple input files")
end end
if length(inputfiles) > 1 && !(inplace || check) if length(inputfiles) > 1 && !(inplace || check)
@ -173,10 +271,8 @@ function main(argv)
push!(inputfiles, "-") push!(inputfiles, "-")
end end
git = ""
if diff if diff
git = something(Sys.which("git"), git) if Sys.which("git") === nothing
if isempty(git)
return panic("option `--diff` requires `git` to be installed") return panic("option `--diff` requires `git` to be installed")
end end
end end
@ -187,7 +283,7 @@ function main(argv)
if input_is_stdin if input_is_stdin
@assert length(inputfiles) == 1 @assert length(inputfiles) == 1
sourcetext = try sourcetext = try
read(stdin, String) read_stdin(String)
catch err catch err
return panic("could not read input from stdin: ", err) return panic("could not read input from stdin: ", err)
end end
@ -205,46 +301,43 @@ function main(argv)
end end
# Figure out output # Figure out output
output_is_file = false
output_is_samefile = false
if inplace if inplace
@assert outputfile === nothing @assert outputfile == ""
@assert isfile(inputfile) @assert isfile(inputfile)
@assert !input_is_stdin @assert !input_is_stdin
output = inputfile output = Output(:file, inputfile, stdout(), true, true)
output_is_samefile = output_is_file = true
elseif check elseif check
@assert outputfile === nothing @assert outputfile == ""
output = devnull output = Output(:devnull, "", stdout(), false, false)
else else
@assert length(inputfiles) == 1 @assert length(inputfiles) == 1
if outputfile === nothing || outputfile == "-" if outputfile == "" || outputfile == "-"
output = stdout output = Output(:stdout, "", stdout(), false, false)
elseif isfile(outputfile) && !input_is_stdin && samefile(outputfile, inputfile) elseif isfile(outputfile) && !input_is_stdin && samefile(outputfile, inputfile)
return panic("can not use same file for input and output, use `-i` to modify a file in place") return panic("can not use same file for input and output, use `-i` to modify a file in place")
else else
output = outputfile output = Output(:file, outputfile, stdout(), true, false)
output_is_file = true
end end
end end
# Print file info unless quiet and unless stdin and/or stdout is involved # Print file info unless quiet and unless stdin and/or stdout is involved
print_progress = !(quiet || input_is_stdin || !(output_is_file || check)) print_progress = !(quiet || input_is_stdin || !(output.output_is_file || check))
# Print file info unless quiet and unless input/output is stdin/stdout # Print file info unless quiet and unless input/output is stdin/stdout
if print_progress if print_progress
@assert inputfile != "-"
input_pretty = relpath(inputfile) input_pretty = relpath(inputfile)
if check if check
str = "Checking `$(input_pretty)` " str = "Checking `$(input_pretty)` "
ndots = 80 - textwidth(str) - 1 - 1 ndots = 80 - textwidth(str) - 1 - 1
dots = ndots > 0 ? "."^ndots : "" dots = ndots > 0 ? "."^ndots : ""
printstyled(stderr, str, dots, " "; color = :blue) printstyled(stderr(), str * dots * " "; color = :blue)
else else
to = output_is_samefile ? " " : " -> `$(relpath(output))` " to = output.output_is_samefile ? " " : " -> `$(relpath(output.file))` "
str = "Formatting `$(inputfile)`$(to)" str = "Formatting `$(inputfile)`$(to)"
ndots = 80 - textwidth(str) - 1 - 1 ndots = 80 - textwidth(str) - 1 - 1
dots = ndots > 0 ? "."^ndots : "" dots = ndots > 0 ? "."^ndots : ""
printstyled(stderr, str, dots, " "; color = :blue) printstyled(stderr(), str * dots * " "; color = :blue)
end end
end end
@ -255,11 +348,20 @@ function main(argv)
ctx′ ctx′
catch err catch err
print_progress && errln() print_progress && errln()
# Limit stacktrace to 5 frames because Runic uses recursion a lot and 5 should if err isa JuliaSyntax.ParseError
# be enough to see where the error occurred. panic("failed to parse input: ", err)
continue
end
msg = "failed to format input: "
@static if juliac
rc = panic(msg, err)
else
# Limit stacktrace to 5 frames because Runic uses recursion a lot and 5
# should be enough to see where the error occurred.
bt = stacktrace(catch_backtrace()) bt = stacktrace(catch_backtrace())
bt = bt[1:min(5, length(bt))] bt = bt[1:min(5, length(bt))]
rc = panic(err, bt) rc = panic(msg, err, bt)
end
if fail_fast if fail_fast
return rc return rc
end end
@ -276,35 +378,58 @@ function main(argv)
print_progress && okln() print_progress && okln()
end end
elseif changed || !inplace elseif changed || !inplace
@assert output !== devnull @assert output.which !== :devnull
try try
write(output, seekstart(ctx.fmt_io)) writeo(output, seekstart(ctx.fmt_io))
catch err catch err
print_progress && errln() print_progress && errln()
panic("could not write to output file `$(output)`: ", err) panic("could not write to output file `$(output.file)`: ", err)
end end
print_progress && okln() print_progress && okln()
else else
print_progress && okln() print_progress && okln()
end end
if diff if diff
@assert git !== ""
mktempdir() do dir mktempdir() do dir
a = mkdir(joinpath(dir, "a")) a = mkdir(joinpath(dir, "a"))
b = mkdir(joinpath(dir, "b")) b = mkdir(joinpath(dir, "b"))
file = basename(inputfile) file = basename(inputfile)
A = joinpath(a, file) A = joinpath(a, file)
B = joinpath(b, file) B = joinpath(b, file)
write(A, ctx.src_str) # juliac: `open(...) do` uses dynamic dispatch otherwise the following
write(B, seekstart(ctx.fmt_io)) # blocks could be written as
cmd = ``` # ```
$(git) --no-pager diff --color=always --no-index --no-prefix # write(A, ctx.src_str)
$(relpath(A, dir)) $(relpath(B, dir)) # write(B, seekstart(ctx.fmt_io))
``` # ```
let io = open(A, "w")
try
write(io, ctx.src_str)
finally
close(io)
end
end
let io = open(B, "w")
try
write(io, seekstart(ctx.fmt_io))
finally
close(io)
end
end
# juliac: Cmd string parsing uses dynamic dispatch
# cmd = ```
# $(git) --no-pager diff --color=always --no-index --no-prefix
# $(relpath(A, dir)) $(relpath(B, dir))
# ```
git_argv = String[
Sys.which("git"), "--no-pager", "diff", "--color=always", "--no-index", "--no-prefix",
relpath(A, dir), relpath(B, dir),
]
cmd = Cmd(git_argv)
# `ignorestatus` because --no-index implies --exit-code # `ignorestatus` because --no-index implies --exit-code
cmd = setenv(ignorestatus(cmd); dir = dir) cmd = setenv(ignorestatus(cmd); dir = dir)
cmd = pipeline(cmd, stdout = stderr, stderr = stderr) cmd = pipeline(cmd, stdout = stderr(), stderr = stderr())
run(cmd) run_cmd(cmd)
end end
end end

5
src/runestone.jl

@ -3245,13 +3245,12 @@ function remove_trailing_semicolon_block(ctx::Context, node::Node)
pos = position(ctx.fmt_io) pos = position(ctx.fmt_io)
kids = verified_kids(node) kids = verified_kids(node)
kids′ = kids kids′ = kids
dealias() = kids′ === kids ? copy(kids) : kids′
semi_idx = findfirst(x -> kind(x) === K";", kids′) semi_idx = findfirst(x -> kind(x) === K";", kids′)
while semi_idx !== nothing while semi_idx !== nothing
search_index = semi_idx + 1 search_index = semi_idx + 1
if kmatch(kids′, KSet"; NewlineWs", semi_idx) if kmatch(kids′, KSet"; NewlineWs", semi_idx)
# `\s*;\n` -> `\n` # `\s*;\n` -> `\n`
kids′ = dealias() kids′ = kids′ === kids ? copy(kids) : kids′
space_before = kmatch(kids′, KSet"Whitespace ;", semi_idx - 1) space_before = kmatch(kids′, KSet"Whitespace ;", semi_idx - 1)
if space_before if space_before
span_overwrite = span(kids′[semi_idx - 1]) + span(kids′[semi_idx]) span_overwrite = span(kids′[semi_idx - 1]) + span(kids′[semi_idx])
@ -3276,7 +3275,7 @@ function remove_trailing_semicolon_block(ctx::Context, node::Node)
kmatch(kids′, KSet"; Whitespace Comment NewlineWs", semi_idx) kmatch(kids′, KSet"; Whitespace Comment NewlineWs", semi_idx)
# `\s*;\s*#\n` -> `\s* \s*#\n` # `\s*;\s*#\n` -> `\s* \s*#\n`
# The `;` is replaced by ` ` here in case comments are aligned # The `;` is replaced by ` ` here in case comments are aligned
kids′ = dealias() kids′ = kids′ === kids ? copy(kids) : kids′
ws_span = span(kids′[semi_idx]) ws_span = span(kids′[semi_idx])
@assert ws_span == 1 @assert ws_span == 1
space_before = kmatch(kids′, KSet"Whitespace ;", semi_idx - 1) space_before = kmatch(kids′, KSet"Whitespace ;", semi_idx - 1)

Loading…
Cancel
Save