Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Runic compilable with juliac #56

Merged
merged 1 commit into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading