Skip to content

Commit 0109cc1

Browse files
committed
WIP: Provide mechanism for Julia syntax evolution
There are several corner cases in the Julia syntax that are essentially bugs or mistakes that we'd like to possibly remove, but can't due to backwards compatibility concerns. Similarly, when adding new syntax features, there are often cases that overlap with valid (but often nonsensical) existing syntax. In the past, we've mostly done judegement calls of these being "minor changes", but as the package ecosystem grows, so does the chance of someone accidentally using these anyway and our "minor changes" have (subjectively) resulted in more breakages recently. Fortunately, all the recent work on making the parser replacable, combined with the fact that JuliaSyntax already supports parsing multiple revisions of Julia syntax provides a solution here: Just let packages declare what version of the Julia syntax they are using. That way, packages would not break if we make changes to the syntax and they can be upgraded at their own pace the next time the author of that particular package upgrades to a new julia version. The way this works is simple. Right now, the parser function is always looked up in `Core._parse`. With this PR, it is instead looked up as `rootmodule(mod)._internal_julia_parse` (slightly longer name to avoid conflicting with existing bindings of the name in downstream packages), or `Core._parse` if no such binding exists. Similar for `_lower`. At the moment, the supported way to make this election is to write `@Base.Experimental.set_syntax_version v"1.14"` (or whatever the version is that you're writing your syntax against). However, to make this truly smooth, I think this should happen automatically through a Project.toml opt-in specifying the expected syntax version. My preference would be to use #59995 if that is merged, but this is a separate feature (with similar motivations around API evolution of course) and there could be a different opt-in mechanism. I should emphasize that I'm not proposing using this for any big syntax revolutions or anything. I would just like to start cleaning up a few corners of the syntax that I think are universally agreed to be bad but that we've kept for backwards compatibility. This way, by the time we get around to making a breaking revision, our entire ecosystem will have already upgraded to the new syntax.
1 parent ed705d8 commit 0109cc1

File tree

9 files changed

+105
-42
lines changed

9 files changed

+105
-42
lines changed

base/client.jl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,8 @@ function eval_user_input(errio, @nospecialize(ast), show_value::Bool)
173173
nothing
174174
end
175175

176-
function _parse_input_line_core(s::String, filename::String)
177-
ex = Meta.parseall(s, filename=filename)
176+
function _parse_input_line_core(s::String, filename::String, mod::Union{Module, Nothing})
177+
ex = Meta.parseall(s; filename, mod)
178178
if ex isa Expr && ex.head === :toplevel
179179
if isempty(ex.args)
180180
return nothing
@@ -189,18 +189,18 @@ function _parse_input_line_core(s::String, filename::String)
189189
return ex
190190
end
191191

192-
function parse_input_line(s::String; filename::String="none", depwarn=true)
192+
function parse_input_line(s::String; filename::String="none", depwarn=true, mod::Union{Module, Nothing}=nothing)
193193
# For now, assume all parser warnings are depwarns
194194
ex = if depwarn
195-
_parse_input_line_core(s, filename)
195+
_parse_input_line_core(s, filename, mod)
196196
else
197197
with_logger(NullLogger()) do
198-
_parse_input_line_core(s, filename)
198+
_parse_input_line_core(s, filename, mod)
199199
end
200200
end
201201
return ex
202202
end
203-
parse_input_line(s::AbstractString) = parse_input_line(String(s))
203+
parse_input_line(s::AbstractString; kwargs...) = parse_input_line(String(s); kwargs...)
204204

205205
# detect the reason which caused an :incomplete expression
206206
# from the error message
@@ -443,7 +443,7 @@ function run_fallback_repl(interactive::Bool)
443443
let input = stdin
444444
if isa(input, File) || isa(input, IOStream)
445445
# for files, we can slurp in the whole thing at once
446-
ex = parse_input_line(read(input, String))
446+
ex = parse_input_line(read(input, String); mod=Main)
447447
if Meta.isexpr(ex, :toplevel)
448448
# if we get back a list of statements, eval them sequentially
449449
# as if we had parsed them sequentially
@@ -466,7 +466,7 @@ function run_fallback_repl(interactive::Bool)
466466
ex = nothing
467467
while !eof(input)
468468
line *= readline(input, keep=true)
469-
ex = parse_input_line(line)
469+
ex = parse_input_line(line; mod=Main)
470470
if !(isa(ex, Expr) && ex.head === :incomplete)
471471
break
472472
end

base/experimental.jl

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,4 +746,48 @@ macro reexport(ex)
746746
return esc(calls)
747747
end
748748

749+
struct VersionedParse
750+
ver::VersionNumber
751+
end
752+
753+
function (vp::VersionedParse)(code, filename::String, lineno::Int, offset::Int, options::Symbol)
754+
if !isdefined(Base, :JuliaSyntax)
755+
if vp.ver === VERSION
756+
return Core._parse
757+
end
758+
error("JuliaSyntax module is required for syntax version $(vp.ver), but it is not loaded.")
759+
end
760+
Base.JuliaSyntax.core_parser_hook(code, filename, lineno, offset, options; syntax_version=vp.ver)
761+
end
762+
763+
struct VersionedLower
764+
ver::VersionNumber
765+
end
766+
767+
function (vp::VersionedLower)(@nospecialize(code), mod::Module,
768+
file="none", line=0, world=typemax(Csize_t), warn=false)
769+
if !isdefined(Base, :JuliaLowering)
770+
if vp.ver === VERSION
771+
return Core._parse
772+
end
773+
error("JuliaLowering module is required for syntax version $(vp.ver), but it is not loaded.")
774+
end
775+
Base.JuliaLowering.core_lowering_hook(code, filename, lineno, offset, options; syntax_version=vp.ver)
776+
end
777+
778+
function set_syntax_version(m::Module, ver::VersionNumber)
779+
if !Base.is_root_module(m)
780+
error("set_syntax_version can only be called on root modules")
781+
end
782+
parser = VersionedParse(ver)
783+
lowerer = VersionedLower(ver)
784+
Core.declare_const(m, :_internal_julia_parse, parser)
785+
Core.declare_const(m, :_internal_julia_lower, lowerer)
786+
nothing
787+
end
788+
789+
macro set_syntax_version(ver)
790+
Expr(:call, set_syntax_version, __module__, esc(ver))
791+
end
792+
749793
end # module

base/meta.jl

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,22 @@ end
304304

305305
ParseError(msg::AbstractString) = ParseError(msg, nothing)
306306

307+
# N.B.: Should match definition in src/ast.c:jl_parse
308+
function parser_for_module(mod::Union{Module, Nothing})
309+
mod === nothing && return Core._parse
310+
mod = Base.moduleroot(mod)
311+
isdefined(mod, :_internal_julia_parse) ?
312+
getglobal(mod, :_internal_julia_parse) :
313+
Core._parse
314+
end
315+
307316
function _parse_string(text::AbstractString, filename::AbstractString,
308-
lineno::Integer, index::Integer, options)
317+
lineno::Integer, index::Integer, options,
318+
_parse=parser_for_module(nothing))
309319
if index < 1 || index > ncodeunits(text) + 1
310320
throw(BoundsError(text, index))
311321
end
312-
ex, offset::Int = Core._parse(text, filename, lineno, index-1, options)
322+
ex, offset::Int = _parse(text, filename, lineno, index-1, options)
313323
ex, offset+1
314324
end
315325

@@ -346,8 +356,8 @@ julia> Meta.parse("(α, β) = 3, 5", 11, greedy=false)
346356
```
347357
"""
348358
function parse(str::AbstractString, pos::Integer;
349-
filename="none", greedy::Bool=true, raise::Bool=true, depwarn::Bool=true)
350-
ex, pos = _parse_string(str, String(filename), 1, pos, greedy ? :statement : :atom)
359+
filename="none", greedy::Bool=true, raise::Bool=true, depwarn::Bool=true, mod = nothing)
360+
ex, pos = _parse_string(str, String(filename), 1, pos, greedy ? :statement : :atom, parser_for_module(mod))
351361
if raise && isexpr(ex, :error)
352362
err = ex.args[1]
353363
if err isa String
@@ -386,8 +396,8 @@ julia> Meta.parse("x = ")
386396
```
387397
"""
388398
function parse(str::AbstractString;
389-
filename="none", raise::Bool=true, depwarn::Bool=true)
390-
ex, pos = parse(str, 1; filename, greedy=true, raise, depwarn)
399+
filename="none", raise::Bool=true, depwarn::Bool=true, mod = nothing)
400+
ex, pos = parse(str, 1; filename, greedy=true, raise, depwarn, mod = mod)
391401
if isexpr(ex, :error)
392402
return ex
393403
end
@@ -398,12 +408,12 @@ function parse(str::AbstractString;
398408
return ex
399409
end
400410

401-
function parseatom(text::AbstractString, pos::Integer; filename="none", lineno=1)
402-
return _parse_string(text, String(filename), lineno, pos, :atom)
411+
function parseatom(text::AbstractString, pos::Integer; filename="none", lineno=1, mod = nothing)
412+
return _parse_string(text, String(filename), lineno, pos, :atom, parser_for_module(mod))
403413
end
404414

405-
function parseall(text::AbstractString; filename="none", lineno=1)
406-
ex,_ = _parse_string(text, String(filename), lineno, 1, :all)
415+
function parseall(text::AbstractString; filename="none", lineno=1, mod = nothing)
416+
ex,_ = _parse_string(text, String(filename), lineno, 1, :all, parser_for_module(mod))
407417
return ex
408418
end
409419

src/ast.c

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,15 +1224,19 @@ JL_DLLEXPORT jl_value_t *jl_fl_lower(jl_value_t *expr, jl_module_t *inmodule,
12241224
JL_DLLEXPORT jl_value_t *jl_lower(jl_value_t *expr, jl_module_t *inmodule,
12251225
const char *filename, int line, size_t world, bool_t warn)
12261226
{
1227-
jl_value_t *core_lower = NULL;
1228-
if (jl_core_module)
1229-
core_lower = jl_get_global_value(jl_core_module, jl_symbol("_lower"), jl_current_task->world_age);
1230-
if (!core_lower || core_lower == jl_nothing) {
1227+
jl_value_t *julia_lower = NULL;
1228+
if (inmodule) {
1229+
jl_module_t *this_root_module = jl_module_root(inmodule);
1230+
julia_lower = jl_get_global(this_root_module, jl_symbol("_internal_julia_lower"));
1231+
}
1232+
if ((!julia_lower || julia_lower == jl_nothing) && jl_core_module)
1233+
julia_lower = jl_get_global_value(jl_core_module, jl_symbol("_lower"), jl_current_task->world_age);
1234+
if (!julia_lower || julia_lower == jl_nothing) {
12311235
return jl_fl_lower(expr, inmodule, filename, line, world, warn);
12321236
}
12331237
jl_value_t **args;
12341238
JL_GC_PUSHARGS(args, 7);
1235-
args[0] = core_lower;
1239+
args[0] = julia_lower;
12361240
args[1] = expr;
12371241
args[2] = (jl_value_t*)inmodule;
12381242
args[3] = jl_cstr_to_string(filename);
@@ -1288,20 +1292,24 @@ jl_code_info_t *jl_inner_ctor_body(jl_array_t *fieldkinds, jl_module_t *inmodule
12881292
// `text` is passed as a pointer to allow raw non-String buffers to be used
12891293
// without copying.
12901294
jl_value_t *jl_parse(const char *text, size_t text_len, jl_value_t *filename,
1291-
size_t lineno, size_t offset, jl_value_t *options)
1295+
size_t lineno, size_t offset, jl_value_t *options, jl_module_t *inmodule)
12921296
{
1293-
jl_value_t *core_parse = NULL;
1294-
if (jl_core_module) {
1295-
core_parse = jl_get_global(jl_core_module, jl_symbol("_parse"));
1297+
jl_value_t *parser = NULL;
1298+
if (inmodule) {
1299+
inmodule = jl_module_root(inmodule);
1300+
parser = jl_get_global(inmodule, jl_symbol("_internal_julia_parse"));
1301+
}
1302+
if ((!parser || parser == jl_nothing) && jl_core_module) {
1303+
parser = jl_get_global(jl_core_module, jl_symbol("_parse"));
12961304
}
1297-
if (!core_parse || core_parse == jl_nothing) {
1305+
if (!parser || parser == jl_nothing) {
12981306
// In bootstrap, directly call the builtin parser.
12991307
jl_value_t *result = jl_fl_parse(text, text_len, filename, lineno, offset, options);
13001308
return result;
13011309
}
13021310
jl_value_t **args;
13031311
JL_GC_PUSHARGS(args, 6);
1304-
args[0] = core_parse;
1312+
args[0] = parser;
13051313
args[1] = (jl_value_t*)jl_alloc_svec(2);
13061314
jl_svecset(args[1], 0, jl_box_uint8pointer((uint8_t*)text));
13071315
jl_svecset(args[1], 1, jl_box_long(text_len));
@@ -1330,7 +1338,7 @@ JL_DLLEXPORT jl_value_t *jl_parse_all(const char *text, size_t text_len,
13301338
{
13311339
jl_value_t *fname = jl_pchar_to_string(filename, filename_len);
13321340
JL_GC_PUSH1(&fname);
1333-
jl_value_t *p = jl_parse(text, text_len, fname, lineno, 0, (jl_value_t*)jl_all_sym);
1341+
jl_value_t *p = jl_parse(text, text_len, fname, lineno, 0, (jl_value_t*)jl_all_sym, NULL);
13341342
JL_GC_POP();
13351343
return jl_svecref(p, 0);
13361344
}
@@ -1343,7 +1351,7 @@ JL_DLLEXPORT jl_value_t *jl_parse_string(const char *text, size_t text_len,
13431351
jl_value_t *fname = jl_cstr_to_string("none");
13441352
JL_GC_PUSH1(&fname);
13451353
jl_value_t *result = jl_parse(text, text_len, fname, 1, offset,
1346-
(jl_value_t*)(greedy ? jl_statement_sym : jl_atom_sym));
1354+
(jl_value_t*)(greedy ? jl_statement_sym : jl_atom_sym), NULL);
13471355
JL_GC_POP();
13481356
return result;
13491357
}

src/julia_internal.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,7 @@ STATIC_INLINE size_t module_usings_max(jl_module_t *m) JL_NOTSAFEPOINT {
932932
}
933933

934934
JL_DLLEXPORT jl_sym_t *jl_module_name(jl_module_t *m) JL_NOTSAFEPOINT;
935+
jl_module_t *jl_module_root(jl_module_t *m);
935936
void jl_add_scanned_method(jl_module_t *m, jl_method_t *meth);
936937
jl_value_t *jl_eval_global_var(jl_module_t *m JL_PROPAGATES_ROOT, jl_sym_t *e, size_t world);
937938
JL_DLLEXPORT jl_value_t *jl_eval_globalref(jl_globalref_t *g, size_t world);
@@ -1366,7 +1367,7 @@ jl_tupletype_t *arg_type_tuple(jl_value_t *arg1, jl_value_t **args, size_t nargs
13661367
JL_DLLEXPORT int jl_has_meta(jl_array_t *body, jl_sym_t *sym) JL_NOTSAFEPOINT;
13671368

13681369
JL_DLLEXPORT jl_value_t *jl_parse(const char *text, size_t text_len, jl_value_t *filename,
1369-
size_t lineno, size_t offset, jl_value_t *options);
1370+
size_t lineno, size_t offset, jl_value_t *options, jl_module_t *inmodule);
13701371
jl_code_info_t *jl_inner_ctor_body(jl_array_t *fieldkinds, jl_module_t *inmodule, const char *file, int line);
13711372
jl_code_info_t *jl_outer_ctor_body(jl_value_t *thistype, size_t nfields, size_t nsparams, jl_module_t *inmodule, const char *file, int line);
13721373
void jl_ctor_def(jl_value_t *ty, jl_value_t *functionloc);

src/timing.c

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
#define DISABLE_FREQUENT_EVENTS
1111
#endif
1212

13-
jl_module_t *jl_module_root(jl_module_t *m);
14-
1513
#ifdef __cplusplus
1614
extern "C" {
1715
#endif

src/toplevel.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,7 @@ static jl_value_t *jl_parse_eval_all(jl_module_t *module, jl_value_t *text,
830830
JL_GC_PUSH3(&ast, &result, &expression);
831831

832832
ast = jl_svecref(jl_parse(jl_string_data(text), jl_string_len(text),
833-
filename, 1, 0, (jl_value_t*)jl_all_sym), 0);
833+
filename, 1, 0, (jl_value_t*)jl_all_sym, module), 0);
834834
if (!jl_is_expr(ast) || ((jl_expr_t*)ast)->head != jl_toplevel_sym) {
835835
jl_errorf("jl_parse_all() must generate a top level expression");
836836
}

stdlib/REPL/src/REPL.jl

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,7 @@ function run_frontend(repl::BasicREPL, backend::REPLBackendRef)
740740
rethrow()
741741
end
742742
end
743-
ast = Base.parse_input_line(line)
743+
ast = Base.parse_input_line(line; mod=Base.active_module(repl))
744744
(isa(ast,Expr) && ast.head === :incomplete) || break
745745
end
746746
if !isempty(line)
@@ -814,7 +814,8 @@ REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
814814
mutable struct ShellCompletionProvider <: CompletionProvider end
815815
struct LatexCompletions <: CompletionProvider end
816816

817-
Base.active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
817+
Base.active_module(mistate::MIState) = mistate.active_module
818+
Base.active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : Base.active_module(mistate)
818819
Base.active_module(::AbstractREPL) = Main
819820
Base.active_module(d::REPLDisplay) = Base.active_module(d.repl)
820821

@@ -1117,7 +1118,7 @@ end
11171118
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
11181119

11191120
function return_callback(s)
1120-
ast = Base.parse_input_line(takestring!(copy(LineEdit.buffer(s))), depwarn=false)
1121+
ast = Base.parse_input_line(takestring!(copy(LineEdit.buffer(s))); mod=Base.active_module(s), depwarn=false)
11211122
return !(isa(ast, Expr) && ast.head === :incomplete)
11221123
end
11231124

@@ -1286,7 +1287,7 @@ function setup_interface(
12861287
repl = repl,
12871288
complete = replc,
12881289
# When we're done transform the entered line into a call to helpmode function
1289-
on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
1290+
on_done = respond(line::String->helpmode(outstream(repl), line, Base.active_module(repl)),
12901291
repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
12911292

12921293

@@ -1367,7 +1368,7 @@ function setup_interface(
13671368
help_mode.hist = hp
13681369
dummy_pkg_mode.hist = hp
13691370

1370-
julia_prompt.on_done = respond(x->Base.parse_input_line(x,filename=repl_filename(repl,hp)), repl, julia_prompt)
1371+
julia_prompt.on_done = respond(x->Base.parse_input_line(x; filename=repl_filename(repl,hp), mod=Base.active_module(repl)), repl, julia_prompt)
13711372

13721373
shell_prompt_len = length(SHELL_PROMPT)
13731374
help_prompt_len = length(HELP_PROMPT)
@@ -1531,7 +1532,7 @@ function setup_interface(
15311532
dump_tail = false
15321533
nl_pos = findfirst('\n', input[oldpos:end])
15331534
if s.current_mode == julia_prompt
1534-
ast, pos = Meta.parse(input, oldpos, raise=false, depwarn=false)
1535+
ast, pos = Meta.parse(input, oldpos, raise=false, depwarn=false, mod=Base.active_module(s))
15351536
if (isa(ast, Expr) && (ast.head === :error || ast.head === :incomplete)) ||
15361537
(pos > ncodeunits(input) && !endswith(input, '\n'))
15371538
# remaining text is incomplete (an error, or parser ran to the end but didn't stop with a newline):
@@ -1787,7 +1788,7 @@ function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
17871788
end
17881789
line = readline(repl.stream, keep=true)
17891790
if !isempty(line)
1790-
ast = Base.parse_input_line(line)
1791+
ast = Base.parse_input_line(line; mod=Base.active_module(repl))
17911792
if have_color
17921793
print(repl.stream, Base.color_normal)
17931794
end

stdlib/REPL/src/REPLCompletions.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,7 @@ end
993993

994994
function completions(string::String, pos::Int, context_module::Module=Main, shift::Bool=true, hint::Bool=false)
995995
# filename needs to be string so macro can be evaluated
996+
# TODO: JuliaSyntax version API here
996997
node = parseall(CursorNode, string, ignore_errors=true, keep_parens=true, filename="none")
997998
cur = @something seek_pos(node, pos) node
998999

0 commit comments

Comments
 (0)