Skip to content

Commit

Permalink
Make Runic compilable with juliac (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
fredrikekre authored Sep 2, 2024
1 parent 65e540a commit 5c017e5
Show file tree
Hide file tree
Showing 9 changed files with 470 additions and 86 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions juliac/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/runicc
26 changes: 26 additions & 0 deletions juliac/Makefile
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions juliac/runicc.jl
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 12 deletions src/Runic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down
15 changes: 13 additions & 2 deletions src/chisels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
203 changes: 203 additions & 0 deletions src/juliac.jl
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 5c017e5

Please sign in to comment.