From 5c017e5524c7005f23938c5df6c660ee3cde4875 Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Mon, 2 Sep 2024 12:31:33 +0200 Subject: [PATCH] Make Runic compilable with juliac (#56) --- Project.toml | 2 +- juliac/.gitignore | 1 + juliac/Makefile | 26 +++++ juliac/runicc.jl | 11 ++ src/Runic.jl | 32 +++--- src/chisels.jl | 15 ++- src/juliac.jl | 203 ++++++++++++++++++++++++++++++++++++ src/main.jl | 261 ++++++++++++++++++++++++++++++++++------------ src/runestone.jl | 5 +- 9 files changed, 470 insertions(+), 86 deletions(-) create mode 100644 juliac/.gitignore create mode 100644 juliac/Makefile create mode 100644 juliac/runicc.jl create mode 100644 src/juliac.jl diff --git a/Project.toml b/Project.toml index e11328f..be01bf2 100644 --- a/Project.toml +++ b/Project.toml @@ -6,7 +6,7 @@ version = "1.0.0" JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" [compat] -JuliaSyntax = "0.4.8" +JuliaSyntax = "0.4.10" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/juliac/.gitignore b/juliac/.gitignore new file mode 100644 index 0000000..ffd8f01 --- /dev/null +++ b/juliac/.gitignore @@ -0,0 +1 @@ +/runicc diff --git a/juliac/Makefile b/juliac/Makefile new file mode 100644 index 0000000..b22f24e --- /dev/null +++ b/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 diff --git a/juliac/runicc.jl b/juliac/runicc.jl new file mode 100644 index 0000000..169fdd6 --- /dev/null +++ b/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 diff --git a/src/Runic.jl b/src/Runic.jl index fd688f6..da9ea2e 100644 --- a/src/Runic.jl +++ b/src/Runic.jl @@ -51,10 +51,18 @@ end # Re-package a GreenNode as a Node function Node(node::JuliaSyntax.GreenNode) tags = 0 % TagType - return Node( - JuliaSyntax.head(node), JuliaSyntax.span(node), - map(Node, JuliaSyntax.children(node)), tags - ) + # juliac: this is `kids = map(Node, JuliaSyntax.children(node))` but written out like + # this in order to help inference. + 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 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 toggle = offmatch.captures[3]::AbstractString 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 end 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 end # 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 if offmatch.captures[1] != onmatch.captures[1] || offmatch.captures[2] != onmatch.captures[2] - @debug "Ignoring `$(onmatch.match)` toggle since it doesn't match the " * - "style of the `$(offmatch.match)` toggle." + # @debug "Ignoring `$(onmatch.match)` toggle since it doesn't match the " * + # "style of the `$(offmatch.match)` toggle." accept_node!(ctx, lkid) continue end toggle = onmatch.captures[3]::AbstractString 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) continue end @assert toggle == "on" 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) continue 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" return typemax(Int) end - @debug "Ignoring `$(offmatch.match)` toggle since no matching `on` toggle " * - "was found at the same tree level." + # @debug "Ignoring `$(offmatch.match)` toggle since no matching `on` toggle " * + # "was found at the same tree level." return nothing end diff --git a/src/chisels.jl b/src/chisels.jl index 5798d7f..acbee17 100644 --- a/src/chisels.jl +++ b/src/chisels.jl @@ -6,8 +6,19 @@ # JuliaSyntax.jl overloads == for this but seems easier to just define a new function function nodes_equal(n1::Node, n2::Node) - return head(n1) == head(n2) && span(n1) == span(n2) && # n1.tags == n2.tags && - all(((x, y),) -> nodes_equal(x, y), zip(n1.kids, n2.kids)) + head(n1) == head(n2) && span(n1) == span(n2) || return false + # 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 # See JuliaSyntax/src/parse_stream.jl diff --git a/src/juliac.jl b/src/juliac.jl new file mode 100644 index 0000000..3c7c178 --- /dev/null +++ b/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 diff --git a/src/main.jl b/src/main.jl index f3a1df3..e275ddd 100644 --- a/src/main.jl +++ b/src/main.jl @@ -6,42 +6,124 @@ else errno = 0 end -function panic(msg...) - printstyled(stderr, "ERROR: "; color = :red, bold = true) - for m in msg - if m isa Exception - showerror(stderr, m) - elseif m isa Vector{Base.StackFrame} - Base.show_backtrace(stderr, m) +# Check whether we are compiling with juliac +# TODO: I thought juliac would never use existing pkgimages but looks like it does so this +# isn't reliable... Existing cache files are pruned in the Makefile for now. +const juliac = let opts = Base.JLOptions() + hasfield(typeof(opts), :trim) && + getfield(opts, :trim) != 0 && + 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 - print(stderr, msg...) + # Ignore it I guess... + end + end + 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(stderr) + println(io) global errno = 1 return errno end -okln() = printstyled(stderr, "✔\n"; color = :green, bold = true) -errln() = printstyled(stderr, "✖\n"; color = :red, bold = true) +function okln() + 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 function print_help() - io = stdout - printstyled(io, "NAME\n", bold = true) + io = stdout() + printstyled(io, "NAME", bold = true) + println(io) println(io, " Runic.main - format Julia source code") 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 [] ...") println(io) - printstyled(io, "DESCRIPTION\n", bold = true) + printstyled(io, "DESCRIPTION", bold = true) + println(io) println( io, """ `Runic.main` (typically invoked as `julia -m Runic`) formats Julia source code using the Runic.jl formatter. """ ) - printstyled(io, "OPTIONS\n", bold = true) + printstyled(io, "OPTIONS", bold = true) + println(io) println( io, """ ... @@ -73,23 +155,39 @@ end function maybe_expand_directory!(outfiles, dir) if !isdir(dir) - # Assumed a file, checked when using it + # Assumed to be a file, checked when using it push!(outfiles, dir) - return + else + scandir!(outfiles, dir) 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 - # inside of `.git` which has the `.jl` extension, but they are not Julia source - # files. - if occursin(".git", root) && ".git" in splitpath(root) - continue - end - for file in files - if endswith(file, ".jl") - push!(outfiles, joinpath(root, file)) + return +end + +# juliac: type-stable output struct (required for juliac but useful in general too) +struct Output{IO} + which::Symbol + file::String + stream::IO + output_is_file::Bool + output_is_samefile::Bool +end + +function writeo(output::Output, iob) + @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 + elseif output.which == :stdout + write(output.stream, iob) end + return end function main(argv) @@ -98,7 +196,7 @@ function main(argv) # Default values inputfiles = String[] - outputfile = nothing + outputfile = "" quiet = false verbose = false debug = false @@ -153,16 +251,16 @@ function main(argv) if inplace && check return panic("options `--inplace` and `--check` are mutually exclusive") end - if inplace && outputfile !== nothing + if inplace && outputfile != "" return panic("options `--inplace` and `--output` are mutually exclusive") end - if check && outputfile !== nothing + if check && outputfile != "" return panic("options `--check` and `--output` are mutually exclusive") end if inplace && input_is_stdin return panic("option `--inplace` can not be used together with stdin input") 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") end if length(inputfiles) > 1 && !(inplace || check) @@ -173,10 +271,8 @@ function main(argv) push!(inputfiles, "-") end - git = "" if diff - git = something(Sys.which("git"), git) - if isempty(git) + if Sys.which("git") === nothing return panic("option `--diff` requires `git` to be installed") end end @@ -187,7 +283,7 @@ function main(argv) if input_is_stdin @assert length(inputfiles) == 1 sourcetext = try - read(stdin, String) + read_stdin(String) catch err return panic("could not read input from stdin: ", err) end @@ -205,46 +301,43 @@ function main(argv) end # Figure out output - output_is_file = false - output_is_samefile = false if inplace - @assert outputfile === nothing + @assert outputfile == "" @assert isfile(inputfile) @assert !input_is_stdin - output = inputfile - output_is_samefile = output_is_file = true + output = Output(:file, inputfile, stdout(), true, true) elseif check - @assert outputfile === nothing - output = devnull + @assert outputfile == "" + output = Output(:devnull, "", stdout(), false, false) else @assert length(inputfiles) == 1 - if outputfile === nothing || outputfile == "-" - output = stdout + if outputfile == "" || outputfile == "-" + output = Output(:stdout, "", stdout(), false, false) 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") else - output = outputfile - output_is_file = true + output = Output(:file, outputfile, stdout(), true, false) end end # 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 if print_progress + @assert inputfile != "-" input_pretty = relpath(inputfile) if check str = "Checking `$(input_pretty)` " ndots = 80 - textwidth(str) - 1 - 1 dots = ndots > 0 ? "."^ndots : "" - printstyled(stderr, str, dots, " "; color = :blue) + printstyled(stderr(), str * dots * " "; color = :blue) else - to = output_is_samefile ? " " : " -> `$(relpath(output))` " + to = output.output_is_samefile ? " " : " -> `$(relpath(output.file))` " str = "Formatting `$(inputfile)`$(to)" ndots = 80 - textwidth(str) - 1 - 1 dots = ndots > 0 ? "."^ndots : "" - printstyled(stderr, str, dots, " "; color = :blue) + printstyled(stderr(), str * dots * " "; color = :blue) end end @@ -255,11 +348,20 @@ function main(argv) ctx′ catch err print_progress && errln() - # 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 = bt[1:min(5, length(bt))] - rc = panic(err, bt) + if err isa JuliaSyntax.ParseError + 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 = bt[1:min(5, length(bt))] + rc = panic(msg, err, bt) + end if fail_fast return rc end @@ -276,35 +378,58 @@ function main(argv) print_progress && okln() end elseif changed || !inplace - @assert output !== devnull + @assert output.which !== :devnull try - write(output, seekstart(ctx.fmt_io)) + writeo(output, seekstart(ctx.fmt_io)) catch err print_progress && errln() - panic("could not write to output file `$(output)`: ", err) + panic("could not write to output file `$(output.file)`: ", err) end print_progress && okln() else print_progress && okln() end if diff - @assert git !== "" mktempdir() do dir a = mkdir(joinpath(dir, "a")) b = mkdir(joinpath(dir, "b")) file = basename(inputfile) A = joinpath(a, file) B = joinpath(b, file) - write(A, ctx.src_str) - write(B, seekstart(ctx.fmt_io)) - cmd = ``` - $(git) --no-pager diff --color=always --no-index --no-prefix - $(relpath(A, dir)) $(relpath(B, dir)) - ``` + # juliac: `open(...) do` uses dynamic dispatch otherwise the following + # blocks could be written as + # ``` + # write(A, ctx.src_str) + # 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 cmd = setenv(ignorestatus(cmd); dir = dir) - cmd = pipeline(cmd, stdout = stderr, stderr = stderr) - run(cmd) + cmd = pipeline(cmd, stdout = stderr(), stderr = stderr()) + run_cmd(cmd) end end diff --git a/src/runestone.jl b/src/runestone.jl index d149c32..c1b4956 100644 --- a/src/runestone.jl +++ b/src/runestone.jl @@ -3245,13 +3245,12 @@ function remove_trailing_semicolon_block(ctx::Context, node::Node) pos = position(ctx.fmt_io) kids = verified_kids(node) kids′ = kids - dealias() = kids′ === kids ? copy(kids) : kids′ semi_idx = findfirst(x -> kind(x) === K";", kids′) while semi_idx !== nothing search_index = semi_idx + 1 if kmatch(kids′, KSet"; NewlineWs", semi_idx) # `\s*;\n` -> `\n` - kids′ = dealias() + kids′ = kids′ === kids ? copy(kids) : kids′ space_before = kmatch(kids′, KSet"Whitespace ;", semi_idx - 1) if space_before 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) # `\s*;\s*#\n` -> `\s* \s*#\n` # The `;` is replaced by ` ` here in case comments are aligned - kids′ = dealias() + kids′ = kids′ === kids ? copy(kids) : kids′ ws_span = span(kids′[semi_idx]) @assert ws_span == 1 space_before = kmatch(kids′, KSet"Whitespace ;", semi_idx - 1)