Skip to content

Commit 734ac30

Browse files
authored
Merge pull request #28 from JuliaLang/cjf/sysimage-fixes
Tool and fixes for compiling a JuliaSyntax sysimage
2 parents 6d446bb + 91a16ad commit 734ac30

File tree

8 files changed

+150
-37
lines changed

8 files changed

+150
-37
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,22 @@ julia> parseall(Expr, "(x + y)*z")
103103
:($(Expr(:toplevel, :((x + y) * z))))
104104
```
105105

106+
# Using JuliaSyntax as the default parser
107+
108+
To use JuliaSyntax as the default Julia parser to `include()` files,
109+
parse code with `Meta.parse()`, etc, call
110+
111+
```
112+
julia> JuliaSyntax.enable_in_core!()
113+
```
114+
115+
This causes some startup latency, so to reduce that you can create a custom
116+
system image by running the code in `./sysimage/compile.jl` as a Julia script
117+
(or directly using the shell, on unix). Then use `julia -J $resulting_sysimage`.
118+
119+
Using a custom sysimage has the advantage that package precompilation will also
120+
go through the JuliaSyntax parser.
121+
106122
# Parser implementation
107123

108124
Our goal is to losslessly represent the source text with a tree; this may be

src/hooks.jl

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
_debug_log = nothing
2+
13
# Adaptor for the API/ABI expected by the Julia runtime code.
24
function core_parser_hook(code, filename, lineno, offset, options)
35
try
@@ -8,17 +10,29 @@ function core_parser_hook(code, filename, lineno, offset, options)
810
(ptr,len) = code
911
code = String(unsafe_wrap(Array, ptr, len))
1012
end
13+
if !isnothing(_debug_log)
14+
print(_debug_log, """
15+
#-#-#-------------------------------
16+
# ENTER filename=$filename, lineno=$lineno, offset=$offset, options=$options"
17+
#-#-#-------------------------------
18+
""")
19+
write(_debug_log, code)
20+
end
21+
1122
io = IOBuffer(code)
1223
seek(io, offset)
1324

1425
stream = ParseStream(io)
1526
rule = options === :all ? :toplevel : options
1627
if rule !== :toplevel
17-
# To copy the flisp parser driver, we ignore leading trivia when
18-
# parsing statements or atoms
28+
# To copy the flisp parser driver, we ignore leading and trailing
29+
# trivia when parsing statements or atoms
1930
bump_trivia(stream)
2031
end
2132
JuliaSyntax.parse(stream; rule=rule)
33+
if rule !== :toplevel
34+
bump_trivia(stream)
35+
end
2236

2337
if any_error(stream)
2438
e = Expr(:error, ParseError(SourceFile(code), stream.diagnostics))
@@ -37,6 +51,14 @@ function core_parser_hook(code, filename, lineno, offset, options)
3751
# of one cancel here.
3852
last_offset = last_byte(stream)
3953

54+
if !isnothing(_debug_log)
55+
println(_debug_log, """
56+
#-#-#-
57+
# EXIT last_offset=$last_offset
58+
#-#-#-
59+
""")
60+
end
61+
4062
# Rewrap result in an svec for use by the C code
4163
return Core.svec(ex, last_offset)
4264
catch exc
@@ -66,17 +88,17 @@ flisp parser for all parsing work.
6688
6789
That is, JuliaSyntax will be used for `include()` `Meta.parse()`, the REPL, etc.
6890
"""
69-
function enable_in_core!()
70-
# TODO: Use invoke_in_world to freeze the world age at the time this was enabled.
71-
Base.eval(Core, :(_parse = $core_parser_hook))
72-
nothing
73-
end
74-
75-
"""
76-
Revert to the flisp parser for all parsing work.
77-
"""
78-
function disable_in_core!()
79-
Base.eval(Core, :(_parse = Core.Compiler.fl_parse))
91+
function enable_in_core!(enable=true)
92+
debug_filename = get(ENV, "JULIA_SYNTAX_DEBUG_FILE", nothing)
93+
global _debug_log
94+
if enable && !isnothing(debug_filename)
95+
_debug_log = open(debug_filename, "w")
96+
elseif !enable && !isnothing(_debug_log)
97+
close(_debug_log)
98+
_debug_log = nothing
99+
end
100+
parser = enable ? core_parser_hook : Core.Compiler.fl_parse
101+
Base.eval(Core, :(_parse = $parser))
80102
nothing
81103
end
82104

src/value_parsing.jl

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -210,41 +210,39 @@ end
210210
# stdlib under the name `Unicode.julia_chartransform`. See
211211
# https://github.com/JuliaLang/julia/pull/42561
212212
#
213-
# To allow use on older Julia versions, we reproduce that logic here.
213+
# To allow use on older Julia versions and to workaround the bug
214+
# https://github.com/JuliaLang/julia/issues/45716
215+
# we reproduce a specialized version of that logic here.
214216

215217
# static wrapper around user callback function
216-
utf8proc_custom_func(codepoint::UInt32, callback::Any) =
217-
UInt32(callback(codepoint))::UInt32
218+
function utf8proc_custom_func(codepoint::UInt32, ::Ptr{Cvoid})::UInt32
219+
(codepoint == 0x025B ? 0x03B5 :
220+
codepoint == 0x00B5 ? 0x03BC :
221+
codepoint == 0x00B7 ? 0x22C5 :
222+
codepoint == 0x0387 ? 0x22C5 :
223+
codepoint == 0x2212 ? 0x002D :
224+
codepoint)
225+
end
218226

219-
function utf8proc_decompose(str, options, buffer, nwords, chartransform::T) where T
220-
ret = ccall(:utf8proc_decompose_custom, Int, (Ptr{UInt8}, Int, Ptr{UInt8}, Int, Cint, Ptr{Cvoid}, Ref{T}),
227+
function utf8proc_decompose(str, options, buffer, nwords)
228+
ret = ccall(:utf8proc_decompose_custom, Int, (Ptr{UInt8}, Int, Ptr{UInt8}, Int, Cint, Ptr{Cvoid}, Ptr{Cvoid}),
221229
str, sizeof(str), buffer, nwords, options,
222-
@cfunction(utf8proc_custom_func, UInt32, (UInt32, Ref{T})), chartransform)
230+
@cfunction(utf8proc_custom_func, UInt32, (UInt32, Ptr{Cvoid})), C_NULL)
223231
ret < 0 && utf8proc_error(ret)
224232
return ret
225233
end
226234

227-
function utf8proc_map(str::Union{String,SubString{String}}, options::Integer, chartransform=identity)
228-
nwords = utf8proc_decompose(str, options, C_NULL, 0, chartransform)
235+
function utf8proc_map(str::Union{String,SubString{String}}, options::Integer)
236+
nwords = utf8proc_decompose(str, options, C_NULL, 0)
229237
buffer = Base.StringVector(nwords*4)
230-
nwords = utf8proc_decompose(str, options, buffer, nwords, chartransform)
238+
nwords = utf8proc_decompose(str, options, buffer, nwords)
231239
nbytes = ccall(:utf8proc_reencode, Int, (Ptr{UInt8}, Int, Cint), buffer, nwords, options)
232240
nbytes < 0 && utf8proc_error(nbytes)
233241
return String(resize!(buffer, nbytes))
234242
end
235243

236-
const _julia_charmap = Dict{UInt32,UInt32}(
237-
0x025B => 0x03B5,
238-
0x00B5 => 0x03BC,
239-
0x00B7 => 0x22C5,
240-
0x0387 => 0x22C5,
241-
0x2212 => 0x002D,
242-
)
243-
244-
julia_chartransform(codepoint::UInt32) = get(_julia_charmap, codepoint, codepoint)
245-
246244
function normalize_identifier(str)
247245
flags = Base.Unicode.UTF8PROC_STABLE | Base.Unicode.UTF8PROC_COMPOSE
248-
utf8proc_map(str, flags, julia_chartransform)
246+
utf8proc_map(str, flags)
249247
end
250248

sysimage/JuliaSyntaxCore/Project.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
name = "JuliaSyntaxCore"
2+
uuid = "05e5f68f-ccd0-4d84-a81a-f557a333a331"
3+
authors = ["Chris Foster <[email protected]> and contributors"]
4+
version = "0.1.0"
5+
6+
[compat]
7+
julia = "1.6"
8+
9+
[deps]
10+
JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module JuliaSyntaxCore
2+
3+
# A tiny module to hold initialization code for JuliaSyntax.jl integration with
4+
# the runtime.
5+
6+
using JuliaSyntax
7+
8+
import Base: JLOptions
9+
10+
function __init__()
11+
# HACK! Fool the runtime into allowing us to set Core._parse, even during
12+
# incremental compilation. (Ideally we'd just arrange for Core._parse to be
13+
# set to the JuliaSyntax parser. But how do we signal that to the dumping
14+
# code outside of the initial creation of Core?)
15+
i = findfirst(==(:incremental), fieldnames(JLOptions))
16+
ptr = convert(Ptr{fieldtype(JLOptions, i)},
17+
cglobal(:jl_options, JLOptions) + fieldoffset(JLOptions, i))
18+
incremental = unsafe_load(ptr)
19+
incremental == 0 || unsafe_store!(ptr, 0)
20+
21+
JuliaSyntax.enable_in_core!()
22+
23+
incremental == 0 || unsafe_store!(ptr, incremental)
24+
end
25+
26+
end

sysimage/compile.jl

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/bin/bash
2+
#=
3+
[[ $1 == +* ]] && juliaup_arg=$1 && shift # release channel for juliaup
4+
exec julia ${juliaup_arg} --startup-file=no -e 'include(popfirst!(ARGS))' "$0" "$@"
5+
=#
6+
7+
imgs_base_path = joinpath(first(DEPOT_PATH), "sysimages", "v$VERSION")
8+
mkpath(imgs_base_path)
9+
10+
using Libdl
11+
12+
cd(@__DIR__)
13+
14+
using Pkg
15+
Pkg.activate(".")
16+
Pkg.develop("JuliaSyntax")
17+
Pkg.develop(path="./JuliaSyntaxCore")
18+
19+
image_path = joinpath(imgs_base_path, "juliasyntax_sysimage."*Libdl.dlext)
20+
21+
using PackageCompiler
22+
PackageCompiler.create_sysimage(
23+
["JuliaSyntaxCore"],
24+
project=".",
25+
sysimage_path=image_path,
26+
precompile_execution_file="precompile_exec.jl",
27+
incremental=true,
28+
)
29+
30+
@info """## System image compiled!
31+
32+
Use it with `julia -J "$image_path"`
33+
"""
34+

sysimage/precompile_exec.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import JuliaSyntax
2+
Base.include(@__MODULE__(), joinpath(pkgdir(JuliaSyntax), "test", "test_utils.jl"))
3+
Base.include(@__MODULE__(), joinpath(pkgdir(JuliaSyntax), "test", "parser.jl"))
4+
JuliaSyntax.enable_in_core!()
5+
@info "Some parsing" Meta.parse("x+y+z-w .+ [a b c]")

test/hooks.jl

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
@test Meta.parse("x + 1") == :(x + 1)
55
@test Meta.parse("x + 1", 1) == (:(x + 1), 6)
66

7-
# Test that parsing statements incrementally works
8-
@test Meta.parse("x + 1\n(y)", 1) == (:(x + 1), 6)
9-
@test Meta.parse("x + 1\n(y)", 6) == (:y, 10)
7+
# Test that parsing statements incrementally works and stops after
8+
# whitespace / comment trivia
9+
@test Meta.parse("x + 1\n(y)\n", 1) == (:(x + 1), 7)
10+
@test Meta.parse("x + 1\n(y)\n", 7) == (:y, 11)
11+
@test Meta.parse(" x#==#", 1) == (:x, 7)
1012

1113
# Check that Meta.parse throws the JuliaSyntax.ParseError rather than
1214
# Meta.ParseError when Core integration is enabled.
1315
@test_throws JuliaSyntax.ParseError Meta.parse("[x")
1416

15-
JuliaSyntax.disable_in_core!()
17+
JuliaSyntax.enable_in_core!(false)
1618
end

0 commit comments

Comments
 (0)