From 38a0486a6dad44b4b03befcadf9346b81b0b3c61 Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Tue, 3 Sep 2024 17:47:08 +0200 Subject: [PATCH] juliac: use a minimal IO implementation on top of raw file descriptors This seems to behave a bit better compared to `Core.stdout` and `Core.stderr`. --- src/juliac.jl | 79 ++++++++++++++++++++++++++++++--------------------- src/main.jl | 56 ++++++++++++++++++------------------ 2 files changed, 75 insertions(+), 60 deletions(-) diff --git a/src/juliac.jl b/src/juliac.jl index e02ab3c..5e0141f 100644 --- a/src/juliac.jl +++ b/src/juliac.jl @@ -1,48 +1,61 @@ # SPDX-License-Identifier: MIT -# juliac-compatible replacement for `read(stdin)` -function read_juliac() +# Minimal juliac-compatible IO implementation for stdin/stdout/stderr +struct RawIO <: Base.IO + fd::RawFD +end + +function Base.unsafe_write(io::RawIO, buf::Ptr{UInt8}, count::UInt) + n = @ccall write(io.fd::Cint, buf::Ptr{Cvoid}, count::Csize_t)::Cssize_t + return n % Int +end + +function Base.write(io::RawIO, byte::UInt8) + n = @ccall write(io.fd::Cint, Ref(byte)::Ptr{Cvoid}, 1::Csize_t)::Cssize_t + return n % Int +end + +# TODO: This could potentially hook into `Base.readbytes!` instead to make this more +# generally useful but for the usecase here we just need to read all the bytes until EOF. +function Base.read(io::RawIO) + @assert io === stdin 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) + bufsize = 1024 + buf = Vector{UInt8}(undef, bufsize) while true - nread = @ccall fread(buf::Ptr{UInt8}, size::Csize_t, nmemb::Cint, file::Ptr{Libc.FILE})::Csize_t + nread = @ccall read(io.fd::Cint, buf::Ptr{Cvoid}, bufsize::Csize_t)::Cssize_t + nread == -1 && systemerror("read") + nread == 0 && break # eof 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 +# juliac-compatible `Base.printstyled` +function printstyled_juliac(io::RawIO, str::String; bold = false, color::Symbol = :normal) + # TODO: Base.printstyled splits on \n and prints each line separately @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") + use_color = isatty(io) + if use_color + 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") + end print(io, str) - bold && write(io, "\e[22m") - color in (:red, :green, :blue) && write(io, "\e[39m") + if use_color + bold && write(io, "\e[22m") + color in (:red, :green, :blue) && write(io, "\e[39m") + end return end +function isatty(io::RawIO) + return (@ccall isatty(io.fd::Cint)::Cint) == 1 +end +supports_color(io::RawIO) = isatty(io) + # juliac-compatible `Base.showerror` function sprint_showerror_juliac(err::Exception) if err isa SystemError @@ -121,10 +134,10 @@ end function run_juliac(cmd::Base.CmdRedirect) # Unpack the redirection layers @assert cmd.stream_no == 2 - @assert cmd.handle::Core.CoreSTDERR === Core.stderr + @assert cmd.handle::RawIO === stderr cmd′ = cmd.cmd::Base.CmdRedirect @assert cmd′.stream_no == 1 - @assert cmd′.handle::Core.CoreSTDERR === Core.stderr + @assert cmd′.handle::RawIO === stderr cmd′′ = cmd′.cmd::Cmd @assert cmd′′.ignorestatus argv = cmd′′.exec diff --git a/src/main.jl b/src/main.jl index 16440b8..f68c314 100644 --- a/src/main.jl +++ b/src/main.jl @@ -11,24 +11,26 @@ using Preferences: @load_preference const juliac = @load_preference("juliac", false) @static if juliac - stderr() = Core.stderr - stdout() = Core.stdout include("juliac.jl") + const stdin = RawIO(RawFD(0)) + const stdout = RawIO(RawFD(1)) + const stderr = RawIO(RawFD(2)) 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 stdin = Base.stdin + # const stdout = Base.stdout + # const stderr = Base.stderr 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 +supports_color(io) = get(io, :color, false) + # 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} @@ -67,39 +69,38 @@ 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) + printstyled(stderr, "ERROR: "; color = :red, bold = true) + print(stderr, msg) if err !== nothing - print(io, sprint_showerror(err)) + print(stderr, sprint_showerror(err)) end @static if juliac @assert bt === nothing else if bt !== nothing - Base.show_backtrace(io, bt) + Base.show_backtrace(stderr, bt) end end - println(io) + println(stderr) global errno = 1 return errno end function okln() - printstyled(stderr(), "✔"; color = :green, bold = true) - println(stderr()) + printstyled(stderr, "✔"; color = :green, bold = true) + println(stderr) return end function errln() - printstyled(stderr(), "✖"; color = :red, bold = true) - println(stderr()) + printstyled(stderr, "✖"; color = :red, bold = true) + println(stderr) return end # Print a typical cli program help message function print_help() - io = stdout() + io = stdout printstyled(io, "NAME", bold = true) println(io) println(io, " Runic.main - format Julia source code") @@ -278,7 +279,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 @@ -300,18 +301,18 @@ function main(argv) @assert outputfile == "" @assert isfile(inputfile) @assert !input_is_stdin - output = Output(:file, inputfile, stdout(), true, true) + output = Output(:file, inputfile, stdout, true, true) elseif check @assert outputfile == "" - output = Output(:devnull, "", stdout(), false, false) + output = Output(:devnull, "", stdout, false, false) else @assert length(inputfiles) == 1 if outputfile == "" || outputfile == "-" - output = Output(:stdout, "", stdout(), false, false) + 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 = Output(:file, outputfile, stdout(), true, false) + output = Output(:file, outputfile, stdout, true, false) end end @@ -326,13 +327,13 @@ function main(argv) 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.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 @@ -411,19 +412,20 @@ function main(argv) close(io) end end + color = supports_color(stderr) ? "always" : "never" # juliac: Cmd string parsing uses dynamic dispatch # cmd = ``` - # $(git) --no-pager diff --color=always --no-index --no-prefix + # $(git) --no-pager diff --color=$(color) --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", + Sys.which("git"), "--no-pager", "diff", "--color=$(color)", "--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()) + cmd = pipeline(cmd, stdout = stderr, stderr = stderr) run_cmd(cmd) end end