diff --git a/Compiler/test/inference.jl b/Compiler/test/inference.jl index ccbcb06a631ba..e7cf8409e3aef 100644 --- a/Compiler/test/inference.jl +++ b/Compiler/test/inference.jl @@ -6463,6 +6463,7 @@ end == TypeError # Issue #58257 - Hang in inference during BindingPartition resolution module A58257 module B58257 + const age = Base.get_world_counter() using ..A58257 # World age here is N end @@ -6474,7 +6475,7 @@ end ## The sequence of events is critical here. A58257.get! # Creates binding partition in A, N+1:∞ A58257.B58257.get! # Creates binding partition in A.B, N+1:∞ -Base.invoke_in_world(UInt(38678), getglobal, A58257, :get!) # Expands binding partition in A through (tuple lam_args...) (block ...))) diff --git a/JuliaLowering/src/eval.jl b/JuliaLowering/src/eval.jl index cef07133b2f5e..645e98530bd1f 100644 --- a/JuliaLowering/src/eval.jl +++ b/JuliaLowering/src/eval.jl @@ -78,19 +78,26 @@ function lower_step(iter, push_mod=nothing) push!(iter.todo, (ex, false, 1)) return lower_step(iter) elseif k == K"module" - name = ex[1] + name_or_version = ex[1] + version = nothing + if kind(name_or_version) == K"VERSION" + version = name_or_version.value + name = ex[2] + else + name = name_or_version + end if kind(name) != K"Identifier" throw(LoweringError(name, "Expected module name")) end newmod_name = Symbol(name.name_val) - body = ex[2] + body = ex[end] if kind(body) != K"block" throw(LoweringError(body, "Expected block in module body")) end std_defs = !has_flags(ex, JuliaSyntax.BARE_MODULE_FLAG) loc = source_location(LineNumberNode, ex) push!(iter.todo, (body, true, 1)) - return Core.svec(:begin_module, newmod_name, std_defs, loc) + return Core.svec(:begin_module, version, newmod_name, std_defs, loc) else # Non macro expansion parts of lowering ctx2, ex2 = expand_forms_2(iter.ctx, ex) @@ -480,9 +487,9 @@ function _eval(mod, iter) break elseif type == :begin_module push!(modules, mod) - filename = something(thunk[4].file, :none) - mod = @ccall jl_begin_new_module(mod::Any, thunk[2]::Symbol, thunk[3]::Cint, - filename::Cstring, thunk[4].line::Cint)::Module + filename = something(thunk[5].file, :none) + mod = @ccall jl_begin_new_module(mod::Any, thunk[3]::Symbol, thunk[2]::Any, thunk[4]::Cint, + filename::Cstring, thunk[5].line::Cint)::Module new_mod = mod elseif type == :end_module @ccall jl_end_new_module(mod::Module)::Cvoid @@ -510,10 +517,11 @@ function _eval(mod, iter, new_mod=nothing) @assert !in_new_mod break elseif type == :begin_module - name = thunk[2]::Symbol - std_defs = thunk[3] + version = thunk[2] + name = thunk[3]::Symbol + std_defs = thunk[4] result = Core.eval(mod, - Expr(:module, std_defs, name, + Expr(:module, (version === nothing ? () : (version,))..., std_defs, name, Expr(:block, thunk[4], Expr(:call, m->_eval(m, iter, m), name))) ) elseif type == :end_module diff --git a/JuliaLowering/test/macros.jl b/JuliaLowering/test/macros.jl index d92b3243a76b4..9e3613f0ee23b 100644 --- a/JuliaLowering/test/macros.jl +++ b/JuliaLowering/test/macros.jl @@ -431,6 +431,7 @@ end ) end """) ≈ @ast_ [K"module" + v"1.14.0"::K"VERSION" "AA"::K"Identifier" [K"block" ] diff --git a/JuliaSyntax/src/integration/expr.jl b/JuliaSyntax/src/integration/expr.jl index 53de5f55f0ee4..06baebf9a33f5 100644 --- a/JuliaSyntax/src/integration/expr.jl +++ b/JuliaSyntax/src/integration/expr.jl @@ -199,7 +199,7 @@ end function parseargs!(retexpr::Expr, loc::LineNumberNode, cursor, source, txtbuf::Vector{UInt8}, txtbuf_offset::UInt32) args = retexpr.args - firstchildhead = head(cursor) + firstchildhead = secondchildhead = head(cursor) firstchildrange::UnitRange{UInt32} = byte_range(cursor) itr = reverse_nontrivia_children(cursor) r = iterate(itr) @@ -208,11 +208,12 @@ function parseargs!(retexpr::Expr, loc::LineNumberNode, cursor, source, txtbuf:: r = iterate(itr, state) expr = node_to_expr(child, source, txtbuf, txtbuf_offset) @assert expr !== nothing + secondchildhead = firstchildhead firstchildhead = head(child) firstchildrange = byte_range(child) pushfirst!(args, fixup_Expr_child(head(cursor), expr, r === nothing)) end - return (firstchildhead, firstchildrange) + return (firstchildhead, secondchildhead, firstchildrange) end _expr_leaf_val(node::SyntaxNode, _...) = node.val @@ -235,6 +236,9 @@ function node_to_expr(cursor, source, txtbuf::Vector{UInt8}, txtbuf_offset::UInt return k == K"error" ? Expr(:error) : Expr(:error, "$(_token_error_descriptions[k]): `$(source[srcrange])`") + elseif k == K"VERSION" + nv = numeric_flags(flags(nodehead)) + return VersionNumber(1, nv ÷ 10, nv % 10) else scoped_val = _expr_leaf_val(cursor, txtbuf, txtbuf_offset) val = @isexpr(scoped_val, :scope_layer) ? scoped_val.args[1] : scoped_val @@ -292,10 +296,11 @@ function node_to_expr(cursor, source, txtbuf::Vector{UInt8}, txtbuf_offset::UInt end # Now recurse to parse all arguments - (firstchildhead, firstchildrange) = parseargs!(retexpr, loc, cursor, source, txtbuf, txtbuf_offset) + (firstchildhead, secondchildhead, firstchildrange) = + parseargs!(retexpr, loc, cursor, source, txtbuf, txtbuf_offset) return _node_to_expr(retexpr, loc, srcrange, - firstchildhead, firstchildrange, + firstchildhead, secondchildhead, firstchildrange, nodehead, source) end @@ -318,7 +323,7 @@ end # tree types. @noinline function _node_to_expr(retexpr::Expr, loc::LineNumberNode, srcrange::UnitRange{UInt32}, - firstchildhead::SyntaxHead, + firstchildhead::SyntaxHead, secondchildhead::SyntaxHead, firstchildrange::UnitRange{UInt32}, nodehead::SyntaxHead, source) @@ -355,6 +360,11 @@ end # Fix up for custom cmd macros like foo`x` args[2] = a2.args[3] end + if kind(secondchildhead) == K"VERSION" + # Encode the syntax version into `loc` so that the argument order + # matches what ordinary macros expect. + loc = Core.MacroSource(loc, popat!(args, 2)) + end end do_lambda = _extract_do_lambda!(args) _reorder_parameters!(args, 2) @@ -554,8 +564,8 @@ end pushfirst!((args[2]::Expr).args, loc) end elseif k == K"module" - pushfirst!(args, !has_flags(nodehead, BARE_MODULE_FLAG)) - pushfirst!((args[3]::Expr).args, loc) + insert!(args, kind(firstchildhead) == K"VERSION" ? 2 : 1, !has_flags(nodehead, BARE_MODULE_FLAG)) + pushfirst!((args[end]::Expr).args, loc) elseif k == K"quote" if length(args) == 1 a1 = only(args) diff --git a/JuliaSyntax/src/integration/hooks.jl b/JuliaSyntax/src/integration/hooks.jl index 2d1e4df852c18..7d5f1781af877 100644 --- a/JuliaSyntax/src/integration/hooks.jl +++ b/JuliaSyntax/src/integration/hooks.jl @@ -162,7 +162,7 @@ end # Debug log file for dumping parsed code const _debug_log = Ref{Union{Nothing,IO}}(nothing) -function core_parser_hook(code, filename::String, lineno::Int, offset::Int, options::Symbol) +function core_parser_hook(code, filename::String, lineno::Int, offset::Int, options::Symbol; syntax_version = v"1.13") try # TODO: Check that we do all this input wrangling without copying the # code buffer @@ -184,7 +184,7 @@ function core_parser_hook(code, filename::String, lineno::Int, offset::Int, opti write(_debug_log[], code) end - stream = ParseStream(code, offset+1) + stream = ParseStream(code, offset+1; version = syntax_version) if options === :statement || options === :atom # To copy the flisp parser driver: # * Parsing atoms consumes leading trivia diff --git a/JuliaSyntax/src/julia/kinds.jl b/JuliaSyntax/src/julia/kinds.jl index dd25663b14ef3..d269aee38e10d 100644 --- a/JuliaSyntax/src/julia/kinds.jl +++ b/JuliaSyntax/src/julia/kinds.jl @@ -247,6 +247,7 @@ register_kinds!(JuliaSyntax, 0, [ "public" "type" "var" + "VERSION" "END_CONTEXTUAL_KEYWORDS" "END_KEYWORDS" diff --git a/JuliaSyntax/src/julia/literal_parsing.jl b/JuliaSyntax/src/julia/literal_parsing.jl index 5a087eac6d54e..f0713b513cbcb 100644 --- a/JuliaSyntax/src/julia/literal_parsing.jl +++ b/JuliaSyntax/src/julia/literal_parsing.jl @@ -408,6 +408,9 @@ function parse_julia_literal(txtbuf::Vector{UInt8}, head::SyntaxHead, srcrange) return had_error ? ErrorVal() : String(take!(io)) elseif k == K"Bool" return txtbuf[first(srcrange)] == u8"t" + elseif k == K"VERSION" + nv = numeric_flags(head) + return VersionNumber(1, nv ÷ 10, nv % 10) end # TODO: Avoid allocating temporary String here diff --git a/JuliaSyntax/src/julia/parser.jl b/JuliaSyntax/src/julia/parser.jl index 4142a010bb6b9..75c806caaa60a 100644 --- a/JuliaSyntax/src/julia/parser.jl +++ b/JuliaSyntax/src/julia/parser.jl @@ -1488,13 +1488,23 @@ function parse_unary_prefix(ps::ParseState, has_unary_prefix=false) end end -function maybe_parsed_macro_name(ps, processing_macro_name, mark) +function maybe_parsed_macro_name(ps, processing_macro_name, last_identifier_orig_kind, mark) if processing_macro_name emit(ps, mark, K"macro_name") + maybe_parsed_special_macro(ps, last_identifier_orig_kind) end return false end +function maybe_parsed_special_macro(ps, last_identifier_orig_kind) + is_syntax_version_macro = last_identifier_orig_kind == K"VERSION" + if is_syntax_version_macro && ps.stream.version >= (1, 14) + # Encode the current parser version into an invisible token + bump_invisible(ps, K"VERSION", + set_numeric_flags(ps.stream.version[2] * 10)) + end +end + # Parses a chain of suffixes at function call precedence, leftmost binding # tightest. This handles # * Bracketed calls like a() b[] c{} @@ -1543,7 +1553,7 @@ function parse_call_chain(ps::ParseState, mark, is_macrocall=false) # @+x y ==> (macrocall (macro_name +) x y) # A.@.x ==> (macrocall (. A (macro_name .)) x) processing_macro_name = maybe_parsed_macro_name( - ps, processing_macro_name, mark) + ps, processing_macro_name, last_identifier_orig_kind, mark) let ps = with_space_sensitive(ps) # Space separated macro arguments # A.@foo a b ==> (macrocall (. A (macro_name foo)) a b) @@ -1577,7 +1587,7 @@ function parse_call_chain(ps::ParseState, mark, is_macrocall=false) # (a=1)() ==> (call (parens (= a 1))) # f (a) ==> (call f (error-t) a) processing_macro_name = maybe_parsed_macro_name( - ps, processing_macro_name, mark) + ps, processing_macro_name, last_identifier_orig_kind, mark) bump_disallowed_space(ps) bump(ps, TRIVIA_FLAG) opts = parse_call_arglist(ps, K")") @@ -1598,7 +1608,7 @@ function parse_call_chain(ps::ParseState, mark, is_macrocall=false) end elseif k == K"[" processing_macro_name = maybe_parsed_macro_name( - ps, processing_macro_name, mark) + ps, processing_macro_name, last_identifier_orig_kind, mark) m = position(ps) # a [i] ==> (ref a (error-t) i) bump_disallowed_space(ps) @@ -1666,7 +1676,7 @@ function parse_call_chain(ps::ParseState, mark, is_macrocall=false) if is_macrocall # Recover by pretending we do have the syntax processing_macro_name = maybe_parsed_macro_name( - ps, processing_macro_name, mark) + ps, processing_macro_name, last_identifier_orig_kind, mark) # @M.(x) ==> (macrocall (dotcall (macro_name M) (error-t) x)) bump_invisible(ps, K"error", TRIVIA_FLAG) emit_diagnostic(ps, mark, @@ -1720,6 +1730,7 @@ function parse_call_chain(ps::ParseState, mark, is_macrocall=false) macro_atname_range = (m, position(ps)) is_macrocall = true emit(ps, mark, K".") + maybe_parsed_special_macro(ps, last_identifier_orig_kind) elseif k == K"'" # f.' => (dotcall-post f (error ')) bump(ps, remap_kind=K"Identifier") # bump ' @@ -1760,7 +1771,7 @@ function parse_call_chain(ps::ParseState, mark, is_macrocall=false) emit(ps, mark, K"call", POSTFIX_OP_FLAG) elseif k == K"{" processing_macro_name = maybe_parsed_macro_name( - ps, processing_macro_name, mark) + ps, processing_macro_name, last_identifier_orig_kind, mark) # Type parameter curlies and macro calls m = position(ps) # S {a} ==> (curly S (error-t) a) @@ -2065,6 +2076,13 @@ function parse_resword(ps::ParseState) # module do \n end ==> (module (error do) (block)) bump(ps, error="Invalid module name") else + if ps.stream.version >= (1, 14) + # Encode the parser version that parsed this module - the runtime + # will use this to set the same parser version for runtime `include` + # etc into this module. + bump_invisible(ps, K"VERSION", + set_numeric_flags(ps.stream.version[2] * 10)) + end # module $A end ==> (module ($ A) (block)) parse_unary_prefix(ps) end diff --git a/JuliaSyntax/src/julia/tokenize.jl b/JuliaSyntax/src/julia/tokenize.jl index 2bd0f56df1b84..0cd27e680cad7 100644 --- a/JuliaSyntax/src/julia/tokenize.jl +++ b/JuliaSyntax/src/julia/tokenize.jl @@ -1245,12 +1245,12 @@ function lex_identifier(l::Lexer, c) end end -# This creates a hash for chars in [a-z] using 5 bit per char. +# This creates a hash for chars in [A-z] using 6 bit per char. # Requires an additional input-length check somewhere, because -# this only works up to ~12 chars. +# this only works up to ~10 chars. @inline function simple_hash(c::Char, h::UInt64) - bytehash = (clamp(c - 'a' + 1, -1, 30) % UInt8) & 0x1f - h << 5 + bytehash + bytehash = (clamp(c - 'A' + 1, -1, 60) % UInt8) & 0x3f + h << 6 + bytehash end function simple_hash(str) @@ -1305,10 +1305,11 @@ K"outer", K"primitive", K"type", K"var", +K"VERSION" ] const _true_hash = simple_hash("true") const _false_hash = simple_hash("false") -const _kw_hash = Dict(simple_hash(lowercase(string(kw))) => kw for kw in kws) +const _kw_hash = Dict(simple_hash(string(kw)) => kw for kw in kws) end # module diff --git a/JuliaSyntax/test/expr.jl b/JuliaSyntax/test/expr.jl index d7547848bef09..a8ec8c68652c3 100644 --- a/JuliaSyntax/test/expr.jl +++ b/JuliaSyntax/test/expr.jl @@ -55,7 +55,7 @@ @test parsestmt("a;b") == Expr(:toplevel, :a, :b) - @test parsestmt("module A\n\nbody\nend") == + @test parsestmt("module A\n\nbody\nend"; version=v"1.13") == Expr(:module, true, :A, @@ -798,9 +798,11 @@ end @testset "module" begin - @test parsestmt("module A end") == + @test parsestmt("module A end"; version=v"1.13") == Expr(:module, true, :A, Expr(:block, LineNumberNode(1), LineNumberNode(1))) - @test parsestmt("baremodule A end") == + @test parsestmt("module A end"; version=v"1.14") == + Expr(:module, v"1.14", true, :A, Expr(:block, LineNumberNode(1), LineNumberNode(1))) + @test parsestmt("baremodule A end"; version=v"1.13") == Expr(:module, false, :A, Expr(:block, LineNumberNode(1), LineNumberNode(1))) end diff --git a/JuliaSyntax/test/parser.jl b/JuliaSyntax/test/parser.jl index 6c66ea40123e0..2b3e106e1dae4 100644 --- a/JuliaSyntax/test/parser.jl +++ b/JuliaSyntax/test/parser.jl @@ -377,6 +377,14 @@ tests = [ "@doc x\n\ny" => "(macrocall (macro_name doc) x)" "@doc x\nend" => "(macrocall (macro_name doc) x)" + # Special 1.14 @VERSION parsing rules + ((v=v"1.13",), "@VERSION") => "(macrocall (macro_name VERSION))" + ((v=v"1.13",), "@A.B.VERSION") => "(macrocall (macro_name (. (. A B) VERSION)))" + ((v=v"1.13",), "A.B.@VERSION") => "(macrocall (. (. A B) (macro_name VERSION)))" + ((v=v"1.14",), "@VERSION") => "(macrocall (macro_name VERSION) v\"1.14.0\")" + ((v=v"1.14",), "@A.B.VERSION") => "(macrocall (macro_name (. (. A B) VERSION)) v\"1.14.0\")" + ((v=v"1.14",), "A.B.@VERSION") => "(macrocall (. (. A B) (macro_name VERSION)) v\"1.14.0\")" + # calls with brackets "f(a,b)" => "(call f a b)" "f(a,)" => "(call-, f a)" diff --git a/JuliaSyntax/test/test_utils.jl b/JuliaSyntax/test/test_utils.jl index ed3d11e2f966f..2ad1ecef7a53c 100644 --- a/JuliaSyntax/test/test_utils.jl +++ b/JuliaSyntax/test/test_utils.jl @@ -210,7 +210,7 @@ function parsers_agree_on_file(text, filename; exprs_equal=exprs_equal_no_linenu return true end try - stream = ParseStream(text) + stream = ParseStream(text; version=v"1.13") parse!(stream) ex = build_tree(Expr, stream, filename=filename) return !JuliaSyntax.any_error(stream) && exprs_equal(fl_ex, ex) diff --git a/JuliaSyntax/test/tokenize.jl b/JuliaSyntax/test/tokenize.jl index fe5bba6ac073e..9675b960a2061 100644 --- a/JuliaSyntax/test/tokenize.jl +++ b/JuliaSyntax/test/tokenize.jl @@ -984,6 +984,7 @@ const all_kws = Set([ "primitive", "type", "var", + "VERSION", # Word-like operators "in", "isa", diff --git a/NEWS.md b/NEWS.md index 83ac6d77ddf1d..40f41d4247c7b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -11,6 +11,10 @@ New language features is now a valid operator with arrow precedence, accessible as `\hookunderrightarrow` at the REPL. ([JuliaLang/JuliaSyntax.jl#525], [#57143]) - Support for Unicode 17 ([#59534]). + - It is now possible to control which version of the Julia syntax will be used to parse a package by setting the + `compat.julia` or `syntax.julia_version` key in Project.toml. This feature is similar to the notion of "editions" + in other language ecosystems and will allow non-breaking evolution of Julia syntax in future versions. + See the "Syntax Versioning" section in the code loading documentation ([#60018]). Language changes ---------------- diff --git a/base/Base_compiler.jl b/base/Base_compiler.jl index 5d786a325940a..781ad5c69d512 100644 --- a/base/Base_compiler.jl +++ b/base/Base_compiler.jl @@ -141,6 +141,20 @@ import Core: @doc, @__doc__, WrappedException, @int128_str, @uint128_str, @big_s # Export list include("exports.jl") +function set_syntax_version end +_topmod(m::Module) = ccall(:jl_base_relative_to, Any, (Any,), m)::Module +function _setup_module!(mod::Module, Core.@nospecialize syntax_ver) + # using Base + Core._using(mod, _topmod(mod), UInt8(0)) + Core.declare_const(mod, :include, IncludeInto(mod)) + Core.declare_const(mod, :eval, Core.EvalInto(mod)) + if syntax_ver === nothing + return nothing + end + set_syntax_version(mod, syntax_ver) + return nothing +end + # core docsystem include("docs/core.jl") Core.atdoc!(CoreDocs.docm) diff --git a/base/boot.jl b/base/boot.jl index f00e3a34f4cdb..daf00f094ff64 100644 --- a/base/boot.jl +++ b/base/boot.jl @@ -1141,4 +1141,10 @@ typename(union::UnionAll) = typename(union.body) include(Core, "optimized_generics.jl") +# Used only be the magic @VERSION macro +struct MacroSource + lno::LineNumberNode + syntax_ver # ::VersionNumber =# +end + ccall(:jl_set_istopmod, Cvoid, (Any, Bool), Core, true) diff --git a/base/client.jl b/base/client.jl index 1b62f870ae1b8..97c0232493c3c 100644 --- a/base/client.jl +++ b/base/client.jl @@ -173,8 +173,8 @@ function eval_user_input(errio, @nospecialize(ast), show_value::Bool) nothing end -function _parse_input_line_core(s::String, filename::String) - ex = Meta.parseall(s, filename=filename) +function _parse_input_line_core(s::String, filename::String, mod::Union{Module, Nothing}) + ex = Meta.parseall(s; filename, mod) if ex isa Expr && ex.head === :toplevel if isempty(ex.args) return nothing @@ -189,18 +189,18 @@ function _parse_input_line_core(s::String, filename::String) return ex end -function parse_input_line(s::String; filename::String="none", depwarn=true) +function parse_input_line(s::String; filename::String="none", depwarn=true, mod::Union{Module, Nothing}=nothing) # For now, assume all parser warnings are depwarns ex = if depwarn - _parse_input_line_core(s, filename) + _parse_input_line_core(s, filename, mod) else with_logger(NullLogger()) do - _parse_input_line_core(s, filename) + _parse_input_line_core(s, filename, mod) end end return ex end -parse_input_line(s::AbstractString) = parse_input_line(String(s)) +parse_input_line(s::AbstractString; kwargs...) = parse_input_line(String(s); kwargs...) # detect the reason which caused an :incomplete expression # from the error message @@ -443,7 +443,7 @@ function run_fallback_repl(interactive::Bool) let input = stdin if isa(input, File) || isa(input, IOStream) # for files, we can slurp in the whole thing at once - ex = parse_input_line(read(input, String)) + ex = parse_input_line(read(input, String); mod=Main) if Meta.isexpr(ex, :toplevel) # if we get back a list of statements, eval them sequentially # as if we had parsed them sequentially @@ -466,7 +466,7 @@ function run_fallback_repl(interactive::Bool) ex = nothing while !eof(input) line *= readline(input, keep=true) - ex = parse_input_line(line) + ex = parse_input_line(line; mod=Main) if !(isa(ex, Expr) && ex.head === :incomplete) break end diff --git a/base/deprecated.jl b/base/deprecated.jl index 241888ba45471..210b2604d4db0 100644 --- a/base/deprecated.jl +++ b/base/deprecated.jl @@ -572,3 +572,14 @@ to_power_type(x) = oftype(x*x, x) @deprecate merge(combine::Callable, d::AbstractDict, others::AbstractDict...) mergewith(combine, d, others...) # end 1.13 deprecations + +# BEGIN 1.14 deprecations + +# Revise calls this +function explicit_manifest_entry_path(args...) + spec = explicit_manifest_entry_load_spec(args...) + spec === nothing && return nothing + return spec.path +end + +# END 1.14 deprecations diff --git a/base/docs/Docs.jl b/base/docs/Docs.jl index 8817d82a6add6..5d1662551ea62 100644 --- a/base/docs/Docs.jl +++ b/base/docs/Docs.jl @@ -306,7 +306,9 @@ elseif head === :call && length(x.args) >= 1 && isexpr(x.args[1], :(::)) # otherwise, for documenting `x::y`, it will be extracted from x astname((x.args[1]::Expr).args[end], ismacro) else - n = if isexpr(x, (:module, :struct)) + n = if isexpr(x, :module) + isa(x.args[1], Bool) ? 2 : 3 + elseif isexpr(x, :struct) 2 elseif isexpr(x, (:call, :macrocall, :function, :(=), :macro, :where, :curly, :(::), :(<:), :(>:), :local, :global, :const, :atomic, @@ -439,9 +441,10 @@ function moduledoc(__source__, __module__, meta, def, def′::Expr) if def === nothing esc(:(Core.eval($name, $(quot(docex))))) else + has_version = !isa(def.args[1], Bool) def = unblock(def) - block = def.args[3].args - if !def.args[1] + block = def.args[3 + has_version].args + if !def.args[1 + has_version] pushfirst!(block, :(import Base: @doc)) end push!(block, docex) diff --git a/base/experimental.jl b/base/experimental.jl index 730a42eb1717d..eb0e9c6c34475 100644 --- a/base/experimental.jl +++ b/base/experimental.jl @@ -746,4 +746,99 @@ macro reexport(ex) return esc(calls) end +struct VersionedParse + ver::VersionNumber +end + +function (vp::VersionedParse)(code, filename::String, lineno::Int, offset::Int, options::Symbol) + if !isdefined(Base, :JuliaSyntax) + if vp.ver === VERSION + return Core._parse + end + error("JuliaSyntax module is required for syntax version $(vp.ver), but it is not loaded.") + end + Base.JuliaSyntax.core_parser_hook(code, filename, lineno, offset, options; syntax_version=vp.ver) +end + +struct VersionedLower + ver::VersionNumber +end + +function (vp::VersionedLower)(@nospecialize(code), mod::Module, + file="none", line=0, world=typemax(Csize_t), warn=false) + if !isdefined(Base, :JuliaLowering) + if vp.ver === VERSION + return Core._parse + end + error("JuliaLowering module is required for syntax version $(vp.ver), but it is not loaded.") + end + Base.JuliaLowering.core_lowering_hook(code, filename, lineno, offset, options; syntax_version=vp.ver) +end + +function Base.set_syntax_version(m::Module, ver::VersionNumber) + parser = VersionedParse(ver) + Core.declare_const(m, :_internal_julia_parse, parser) + #lowerer = VersionedLower(ver) + #Core.declare_const(m, :_internal_julia_lower, lowerer) + nothing +end + +""" + Base.Experimental.@set_syntax_version ver + +Sets the syntax version to the current module to `ver`. This overrides settings of `syntax.julia_version` or +`compat.julia` from Project.toml. + +!!! compat "Julia 1.14" + This macro was added in Julia 1.14. + +!!! warning + The new syntax version will take effect only for code parsed after the *invocation* of the result of the macro + expansion. This may be unintuitive if the macro is used inside a module body, as the entire module will be parsed + before any statements therein are executed, e.g. consider. + + ``` + @set_syntax_version v"1.13" + module ChangeSyntax + @set_syntax_version v"1.14" + expr1 # Parsed with syntax version 1.13 + # The call itself is parsed with syntax version 1.13, but the included code is parsed with syntax version 1.14 + include_string(ChangeSyntax, "expr2") + expr3 # Parsed with syntax version 1.13 + end + ``` + + For this reason, the Project.toml mechanism is strongly preferred for packages. + However, this macro may be useful for scripts or the REPL. + +!!! warning + This interface is experimental and subject to change or removal without notice. +""" +macro set_syntax_version(ver) + Expr(:call, Base.set_syntax_version, __module__, esc(ver)) +end + +""" + Base.Experimental.@VERSION ver + +This macro provides access to parser (and possibly in the future other frontend component) language version +information. In particular, `(@VERSION).syntax` provides the syntax version used to parse the location where the macro is invoked. + +!!! compat "Julia 1.14" + This macro was added in Julia 1.14. + +!!! note + Calls to this macro have special handling in the parser and the name `@VERSION` is mandatory. At this time, other macros do not + have access to source syntax version information. +""" +function var"@VERSION"(__source__::Union{LineNumberNode, Core.MacroSource}, __module__::Module) + # This macro has special handling in the parser, which puts the current syntax + # version into __source__. + if isa(__source__, LineNumberNode) + return :((; syntax = v"1.13", runtime = VERSION)) + else + return :((; syntax = $(__source__.syntax_ver), runtime = VERSION)) + end +end + end # module diff --git a/base/loading.jl b/base/loading.jl index 06871df9056d0..adbdae07413f0 100644 --- a/base/loading.jl +++ b/base/loading.jl @@ -249,6 +249,17 @@ function get_updated_dict(p::TOML.Parser, f::CachedTOMLDict) return f.d end +""" + struct PkgLoadSpec + +A PkgLoadSpec is the result of a `locate_package` operation and specifies how +and wherefrom to load a julia package. +""" +struct PkgLoadSpec + path::String + julia_syntax_version::VersionNumber +end + struct LoadingCache load_path::Vector{String} dummy_uuid::Dict{String, UUID} @@ -257,7 +268,7 @@ struct LoadingCache require_parsed::Set{String} identified_where::Dict{Tuple{PkgId, String}, Union{Nothing, Tuple{PkgId, String}}} identified::Dict{String, Union{Nothing, Tuple{PkgId, String}}} - located::Dict{Tuple{PkgId, Union{String, Nothing}}, Union{Tuple{String, String}, Nothing}} + located::Dict{Tuple{PkgId, Union{String, Nothing}}, Union{Tuple{PkgLoadSpec, String}, Nothing}} end const LOADING_CACHE = Ref{Union{LoadingCache, Nothing}}(nothing) # n.b.: all access to and through this are protected by require_lock LoadingCache() = LoadingCache(load_path(), Dict(), Dict(), Dict(), Set(), Dict(), Dict(), Dict()) @@ -430,26 +441,28 @@ identify_package(where::Module, name::String) = @lock require_lock _nothing_or_f identify_package(where::PkgId, name::String) = @lock require_lock _nothing_or_first(identify_package_env(where, name)) identify_package(name::String) = @lock require_lock _nothing_or_first(identify_package_env(name)) -function locate_package_env(pkg::PkgId, stopenv::Union{String, Nothing}=nothing)::Union{Nothing,Tuple{String,String}} +function locate_package_env(pkg::PkgId, stopenv::Union{String, Nothing}=nothing)::Union{Nothing,Tuple{PkgLoadSpec, String}} assert_havelock(require_lock) cache = LOADING_CACHE[] if cache !== nothing - pathenv = get(cache.located, (pkg, stopenv), missing) - pathenv === missing || return pathenv + specenv = get(cache.located, (pkg, stopenv), missing) + specenv === missing || return specenv end - path = nothing + spec = nothing env′ = nothing + syntax_version = VERSION if pkg.uuid === nothing # The project we're looking for does not have a Project.toml (n.b. - present # `Project.toml` without UUID gets a path-based dummy UUID). It must have # come from an implicit manifest environment, so go through those only. + # N.B.: Implicitly loaded packages do not participate in syntax versioning. for env in load_path() project_file = env_project_file(env) (project_file isa Bool && project_file) || continue found = implicit_manifest_pkgid(env, pkg.name) if found !== nothing && found.uuid === nothing @assert found.name == pkg.name - path = implicit_manifest_uuid_path(env, pkg) + spec = implicit_manifest_uuid_load_spec(env, pkg) env′ = env @goto done end @@ -459,13 +472,13 @@ function locate_package_env(pkg::PkgId, stopenv::Union{String, Nothing}=nothing) end else for env in load_path() - path = manifest_uuid_path(env, pkg) + spec = manifest_uuid_load_spec(env, pkg) # missing is used as a sentinel to stop looking further down in envs - if path === missing - path = nothing + if spec === missing + spec = nothing @goto done end - if path !== nothing + if spec !== nothing env′ = env @goto done end @@ -475,22 +488,22 @@ function locate_package_env(pkg::PkgId, stopenv::Union{String, Nothing}=nothing) end # Allow loading of stdlibs if the name/uuid are given # e.g. if they have been explicitly added to the project/manifest - mbypath = manifest_uuid_path(Sys.STDLIB, pkg) - if mbypath isa String - path = mbypath + mbyspec = manifest_uuid_load_spec(Sys.STDLIB, pkg) + if mbyspec isa PkgLoadSpec + spec = mbyspec env′ = Sys.STDLIB @goto done end end @label done - if path !== nothing && !isfile_casesensitive(path) - path = nothing + if spec !== nothing && !isfile_casesensitive(spec.path) + spec = nothing end if cache !== nothing - cache.located[(pkg, stopenv)] = path === nothing ? nothing : (path, something(env′)) + cache.located[(pkg, stopenv)] = spec === nothing ? nothing : (spec, something(env′)) end - path === nothing && return nothing - return path, something(env′) + spec === nothing && return nothing + return spec, something(env′) end """ @@ -508,7 +521,19 @@ julia> Base.locate_package(pkg) ``` """ function locate_package(pkg::PkgId, stopenv::Union{String, Nothing}=nothing)::Union{Nothing,String} - @lock require_lock _nothing_or_first(locate_package_env(pkg, stopenv)) + @lock require_lock begin + specenv = locate_package_env(pkg, stopenv) + specenv === nothing && return nothing + specenv[1].path + end +end + +function locate_package_load_spec(pkg::PkgId, stopenv::Union{String, Nothing}=nothing)::Union{Nothing,PkgLoadSpec} + @lock require_lock begin + specenv = locate_package_env(pkg, stopenv) + specenv === nothing && return nothing + specenv[1] + end end """ @@ -808,21 +833,22 @@ function environment_deps_get(env::String, where::Union{Nothing,PkgId}, name::St return explicit_manifest_deps_get(project_file, where, name) end -function manifest_uuid_path(env::String, pkg::PkgId)::Union{Nothing,String,Missing} +function manifest_uuid_load_spec(env::String, pkg::PkgId)::Union{Nothing,PkgLoadSpec,Missing} project_file = env_project_file(env) if project_file isa String proj = project_file_name_uuid(project_file, pkg.name) if proj == pkg # if `pkg` matches the project, return the project itself - return project_file_path(project_file, pkg.name) + return project_file_load_spec(project_file, pkg.name) end - mby_ext = project_file_ext_path(project_file, pkg) + mby_ext = project_file_ext_load_spec(project_file, pkg) mby_ext === nothing || return mby_ext # look for manifest file and `where` stanza - return explicit_manifest_uuid_path(project_file, pkg) + return explicit_manifest_uuid_load_spec(project_file, pkg) elseif project_file # if env names a directory, search it - proj = implicit_manifest_uuid_path(env, pkg) + # Implicit environments do not participate in syntax versioning + proj = implicit_manifest_uuid_load_spec(env, pkg) proj === nothing || return proj # if not found, this might be an extension - first we fast path needing # to scan the whole directory for a matching extension by peeking at @@ -835,14 +861,14 @@ function manifest_uuid_path(env::String, pkg::PkgId)::Union{Nothing,String,Missi if parent_project_file !== nothing parentproj = project_file_name_uuid(parent_project_file, parentid.name) if parentproj == parentid - mby_ext = project_file_ext_path(parent_project_file, pkg) + mby_ext = project_file_ext_load_spec(parent_project_file, pkg) mby_ext === nothing || return mby_ext end end else # We still need to scan the whole directory for extensions. - ext_path, ext_proj = implicit_env_project_file_extension(env, pkg) - ext_path === nothing || return ext_path + ext_ls, ext_proj = implicit_env_project_file_extension(env, pkg) + ext_ls === nothing || return ext_ls end end return nothing @@ -855,13 +881,14 @@ function find_ext_path(project_path::String, extname::String) return joinpath(project_path, "ext", extname * ".jl") end -function project_file_ext_path(project_file::String, ext::PkgId) +function project_file_ext_load_spec(project_file::String, ext::PkgId) d = parsed_toml(project_file) p = dirname(project_file) exts = get(d, "extensions", nothing)::Union{Dict{String, Any}, Nothing} if exts !== nothing if ext.name in keys(exts) && ext.uuid == uuid5(UUID(d["uuid"]::String), ext.name) - return find_ext_path(p, ext.name) + # Syntax version of the main package applies to its extensions + return PkgLoadSpec(find_ext_path(p, ext.name), project_get_syntax_version(d)) end end return nothing @@ -876,14 +903,47 @@ function project_file_name_uuid(project_file::String, name::String)::PkgId return PkgId(uuid, name) end -function project_file_path(project_file::String, name::String) +const NON_VERSIONED_SYNTAX = v"1.13" + +function project_get_syntax_version(d::Dict) + # Syntax Evolution. First check syntax.julia_version entry + sv = nothing + ds = get(d, "syntax", nothing) + if ds !== nothing + sv = VersionNumber(get(ds, "julia_version", nothing)) + end + # If not found, default to minimum(compat["julia"]) + if sv === nothing + cs = get(d, "compat", nothing) + if cs !== nothing + jv = get(cs, "julia", nothing) + if jv !== nothing + sv = VersionNumber(minimum(semver_spec(jv)).t...) + end + end + end + # Finally, if neither of those are set, default to the current Julia version. + # N.B.: This choice is less "compatible" than defaulting to a fixed older version. + # However, it avoids surprises from moving over scripts and REPL code to packages + if sv === nothing + sv = VERSION + elseif sv <= NON_VERSIONED_SYNTAX + # Syntax versioning was first introduced in Julia 1.14 - we do not support + # going back to versions before syntax version 1.13. + sv = NON_VERSIONED_SYNTAX + end + return sv +end + +function project_file_load_spec(project_file::String, name::String) d = parsed_toml(project_file) entryfile = get(d, "path", nothing)::Union{String, Nothing} # "path" entry in project file is soft deprecated if entryfile === nothing entryfile = get(d, "entryfile", nothing)::Union{String, Nothing} end - return entry_path(dirname(project_file), name, entryfile) + sv = project_get_syntax_version(d) + return PkgLoadSpec(entry_path(dirname(project_file), name, entryfile), sv) end function workspace_manifest(project_file) @@ -966,9 +1026,9 @@ function implicit_env_project_file_extension(dir::String, ext::PkgId) for pkg in readdir(dir; join=true) project_file = env_project_file(pkg) project_file isa String || continue - path = project_file_ext_path(project_file, ext) - if path !== nothing - return path, project_file + ls = project_file_ext_load_spec(project_file, ext) + if ls !== nothing + return ls, project_file end end return nothing, nothing @@ -1113,7 +1173,7 @@ function explicit_manifest_deps_get(project_file::String, where::PkgId, name::St end # find `uuid` stanza, return the corresponding path -function explicit_manifest_uuid_path(project_file::String, pkg::PkgId)::Union{Nothing,String,Missing} +function explicit_manifest_uuid_load_spec(project_file::String, pkg::PkgId)::Union{Nothing,PkgLoadSpec,Missing} manifest_file = project_file_manifest_path(project_file) manifest_file === nothing && return nothing # no manifest, skip env @@ -1125,7 +1185,7 @@ function explicit_manifest_uuid_path(project_file::String, pkg::PkgId)::Union{No uuid = get(entry, "uuid", nothing)::Union{Nothing, String} uuid === nothing && continue if UUID(uuid) === pkg.uuid - return explicit_manifest_entry_path(manifest_file, pkg, entry) + return explicit_manifest_entry_load_spec(manifest_file, pkg, entry) end end end @@ -1137,30 +1197,46 @@ function explicit_manifest_uuid_path(project_file::String, pkg::PkgId)::Union{No uuid = get(entry, "uuid", nothing)::Union{Nothing, String} extensions = get(entry, "extensions", nothing)::Union{Nothing, Dict{String, Any}} if extensions !== nothing && haskey(extensions, pkg.name) && uuid !== nothing && uuid5(UUID(uuid), pkg.name) == pkg.uuid - parent_path = explicit_manifest_entry_path(manifest_file, PkgId(UUID(uuid), name), entry) - if parent_path === nothing || parent_path === missing + parent_load_spec = explicit_manifest_entry_load_spec(manifest_file, PkgId(UUID(uuid), name), entry) + if parent_load_spec === nothing || parent_load_spec === missing error("failed to find source of parent package: \"$name\"") end + parent_path = parent_load_spec.path p = normpath(dirname(parent_path), "..") - return find_ext_path(p, pkg.name) + return PkgLoadSpec(find_ext_path(p, pkg.name), parent_load_spec.julia_syntax_version) end end end return nothing end -function explicit_manifest_entry_path(manifest_file::String, pkg::PkgId, entry::Dict{String,Any}) +function explicit_manifest_entry_load_spec(manifest_file::String, pkg::PkgId, entry::Dict{String,Any})::Union{Nothing, Missing, PkgLoadSpec} + # Resolve syntax version. N.B.: Unlike in project files, an absent syntax.julia_version + # entry in manifest files means defaulting to 1.13. This is because we assume the + # manifest was created by an older version of julia that did not support syntax versioning. + # Newer versions of Pkg will provide syntax version information in the manifest, + # even if absent from the project file. + syntax_version = NON_VERSIONED_SYNTAX + syntax_table = get(entry, "syntax", nothing) + if syntax_table !== nothing + syntax_version = VersionNumber(get(syntax_table, "julia_version", nothing)) + end + + # Resolve path path = get(entry, "path", nothing)::Union{Nothing, String} entryfile = get(entry, "entryfile", nothing)::Union{Nothing, String} if path !== nothing path = entry_path(normpath(abspath(dirname(manifest_file), path)), pkg.name, entryfile) - return path + return PkgLoadSpec(path, syntax_version) end hash = get(entry, "git-tree-sha1", nothing)::Union{Nothing, String} if hash === nothing - mbypath = manifest_uuid_path(Sys.STDLIB, pkg) - if mbypath isa String && isfile(mbypath) - return mbypath + # stdlibs do not have a git-hash so cannot be loaded from depots. As + # a special case, we allow loading these directly from the stdlib location + # (treated as an implicit environment). + mbyspec = manifest_uuid_load_spec(Sys.STDLIB, pkg) + if mbyspec isa PkgLoadSpec && isfile(mbyspec.path) + return mbyspec end return nothing end @@ -1170,7 +1246,7 @@ function explicit_manifest_entry_path(manifest_file::String, pkg::PkgId, entry:: for slug in (version_slug(uuid, hash), version_slug(uuid, hash, 4)) for depot in DEPOT_PATH path = joinpath(depot, "packages", pkg.name, slug) - ispath(path) && return entry_path(abspath(path), pkg.name, entryfile) + ispath(path) && return PkgLoadSpec(entry_path(abspath(path), pkg.name, entryfile), syntax_version) end end # no depot contains the package, return missing to stop looking @@ -1202,15 +1278,16 @@ function implicit_manifest_project(dir::String, pkg::PkgId)::Union{Nothing, Stri end # look for an entry-point for `pkg` and return its path if UUID matches -function implicit_manifest_uuid_path(dir::String, pkg::PkgId)::Union{Nothing,String} +function implicit_manifest_uuid_load_spec(dir::String, pkg::PkgId)::Union{Nothing, PkgLoadSpec} path, project_file = entry_point_and_project_file(dir, pkg.name) if project_file === nothing pkg.uuid === nothing || return nothing - return path + # Without a project file, treat as empty - which defaults to VERSION + return PkgLoadSpec(path, VERSION) end proj = project_file_name_uuid(project_file, pkg.name) proj == pkg || return nothing - return path + return PkgLoadSpec(path, project_get_syntax_version(parsed_toml(project_file))) end ## other code loading functionality ## @@ -1307,7 +1384,7 @@ function _include_from_serialized(pkg::PkgId, path::String, ocachepath::Union{No for i in eachindex(depmods) dep = depmods[i] dep isa Module && continue - _, depkey, depbuild_id = dep::Tuple{String, PkgId, UInt128} + _, depkey, depbuild_id = dep::Tuple{PkgLoadSpec, PkgId, UInt128} dep = something(maybe_loaded_precompile(depkey, depbuild_id)) @assert PkgId(dep) == depkey && module_build_id(dep) === depbuild_id depmods[i] = dep @@ -1515,7 +1592,7 @@ function insert_extension_triggers(pkg::PkgId) pkg.uuid === nothing && return path_env_loc = locate_package_env(pkg) path_env_loc === nothing && return - path, env_loc = path_env_loc + _, env_loc = path_env_loc insert_extension_triggers(env_loc, pkg) end @@ -1875,16 +1952,16 @@ function show(io::IO, it::ImageTarget) end # should sync with the types of arguments of `stale_cachefile` -const StaleCacheKey = Tuple{PkgId, UInt128, String, String, Bool, CacheFlags} +const StaleCacheKey = Tuple{PkgId, UInt128, PkgLoadSpec, String, Bool, CacheFlags} function compilecache_freshest_path(pkg::PkgId; ignore_loaded::Bool=false, stale_cache::Dict{StaleCacheKey,Bool}=Dict{StaleCacheKey, Bool}(), cachepath_cache::Dict{PkgId, Vector{String}}=Dict{PkgId, Vector{String}}(), cachepaths::Vector{String}=get(() -> find_all_in_cache_path(pkg), cachepath_cache, pkg), - sourcepath::Union{String,Nothing}=Base.locate_package(pkg), + sourcespec::Union{PkgLoadSpec,Nothing}=Base.locate_package_load_spec(pkg), flags::CacheFlags=CacheFlags()) - isnothing(sourcepath) && error("Cannot locate source for $(repr("text/plain", pkg))") + isnothing(sourcespec) && error("Cannot locate source for $(repr("text/plain", pkg))") try_build_ids = UInt128[UInt128(0)] if !ignore_loaded let loaded = get(loaded_precompiles, pkg, nothing) @@ -1897,7 +1974,7 @@ function compilecache_freshest_path(pkg::PkgId; end for build_id in try_build_ids for path_to_try in cachepaths - staledeps = stale_cachefile(pkg, build_id, sourcepath, path_to_try; ignore_loaded, requested_flags=flags) + staledeps = stale_cachefile(pkg, build_id, sourcespec, path_to_try; ignore_loaded, requested_flags=flags) if staledeps === true continue end @@ -1905,11 +1982,11 @@ function compilecache_freshest_path(pkg::PkgId; # finish checking staledeps module graph for dep in staledeps dep isa Module && continue - modpath, modkey, modbuild_id = dep::Tuple{String, PkgId, UInt128} + modspec, modkey, modbuild_id = dep::Tuple{PkgLoadSpec, PkgId, UInt128} modpaths = get(() -> find_all_in_cache_path(modkey), cachepath_cache, modkey) for modpath_to_try in modpaths::Vector{String} - stale_cache_key = (modkey, modbuild_id, modpath, modpath_to_try, ignore_loaded, flags)::StaleCacheKey - if get!(() -> stale_cachefile(modkey, modbuild_id, modpath, modpath_to_try; ignore_loaded, requested_flags=flags) === true, + stale_cache_key = (modkey, modbuild_id, modspec, modpath_to_try, ignore_loaded, flags)::StaleCacheKey + if get!(() -> stale_cachefile(modkey, modbuild_id, modspec, modpath_to_try; ignore_loaded, requested_flags=flags) === true, stale_cache, stale_cache_key) continue end @@ -1985,6 +2062,7 @@ function parse_cache_buildid(cachepath::String) checksum = isvalid_cache_header(f) iszero(checksum) && throw(ArgumentError("Incompatible header in cache file $cachefile.")) flags = read(f, UInt8) + syntax_version = read(f, UInt8) n = read(f, Int32) n == 0 && error("no module defined in $cachefile") skip(f, n) # module name @@ -2002,11 +2080,10 @@ function _tryrequire_from_serialized(modkey::PkgId, build_id::UInt128) loaded = start_loading(modkey, build_id, false) if loaded === nothing try - modpath = locate_package(modkey) - isnothing(modpath) && error("Cannot locate source for $(repr("text/plain", modkey))") - modpath = String(modpath)::String - set_pkgorigin_version_path(modkey, modpath) - loaded = _require_search_from_serialized(modkey, modpath, build_id, true) + modspec = locate_package_load_spec(modkey) + isnothing(modspec) && error("Cannot locate source for $(repr("text/plain", modkey))") + set_pkgorigin_version_path(modkey, modspec.path) + loaded = _require_search_from_serialized(modkey, modspec, build_id, true) finally end_loading(modkey, loaded) end @@ -2063,7 +2140,7 @@ end # returns `nothing` if require found a precompile cache for this sourcepath, but couldn't load it or it was stale # returns the set of modules restored if the cache load succeeded -@constprop :none function _require_search_from_serialized(pkg::PkgId, sourcepath::String, build_id::UInt128, stalecheck::Bool; reasons=nothing, DEPOT_PATH::typeof(DEPOT_PATH)=DEPOT_PATH) +@constprop :none function _require_search_from_serialized(pkg::PkgId, sourcespec::PkgLoadSpec, build_id::UInt128, stalecheck::Bool; reasons=nothing, DEPOT_PATH::typeof(DEPOT_PATH)=DEPOT_PATH) assert_havelock(require_lock) paths = find_all_in_cache_path(pkg, DEPOT_PATH) newdeps = PkgId[] @@ -2079,7 +2156,7 @@ end end for build_id in try_build_ids for path_to_try in paths::Vector{String} - staledeps = stale_cachefile(pkg, build_id, sourcepath, path_to_try; reasons, stalecheck) + staledeps = stale_cachefile(pkg, build_id, sourcespec, path_to_try; reasons, stalecheck) if staledeps === true continue end @@ -2097,7 +2174,7 @@ end i += 1 dep = staledeps[i] dep isa Module && continue - _, modkey, modbuild_id = dep::Tuple{String, PkgId, UInt128} + _, modkey, modbuild_id = dep::Tuple{PkgLoadSpec, PkgId, UInt128} dep = canstart_loading(modkey, modbuild_id, stalecheck) if dep isa Module if PkgId(dep) == modkey && module_build_id(dep) === modbuild_id @@ -2118,19 +2195,19 @@ end for i in reverse(eachindex(staledeps)) dep = staledeps[i] dep isa Module && continue - modpath, modkey, modbuild_id = dep::Tuple{String, PkgId, UInt128} + modspec, modkey, modbuild_id = dep::Tuple{PkgLoadSpec, PkgId, UInt128} # inline a call to start_loading here @assert canstart_loading(modkey, modbuild_id, stalecheck) === nothing package_locks[modkey] = (current_task(), Threads.Condition(require_lock), modbuild_id) startedloading = i modpaths = find_all_in_cache_path(modkey, DEPOT_PATH) for modpath_to_try in modpaths - modstaledeps = stale_cachefile(modkey, modbuild_id, modpath, modpath_to_try; stalecheck) + modstaledeps = stale_cachefile(modkey, modbuild_id, modspec, modpath_to_try; stalecheck) if modstaledeps === true continue end modstaledeps, modocachepath, _ = modstaledeps::Tuple{Vector{Any}, Union{Nothing, String}, UInt128} - staledeps[i] = (modpath, modkey, modbuild_id, modpath_to_try, modstaledeps, modocachepath) + staledeps[i] = (modspec, modkey, modbuild_id, modpath_to_try, modstaledeps, modocachepath) @goto check_next_dep end @debug "Rejecting cache file $path_to_try because required dependency $modkey with build ID $(UUID(modbuild_id)) is missing from the cache." @@ -2154,8 +2231,8 @@ end for i in eachindex(staledeps) dep = staledeps[i] dep isa Module && continue - modpath, modkey, modbuild_id, modcachepath, modstaledeps, modocachepath = dep::Tuple{String, PkgId, UInt128, String, Vector{Any}, Union{Nothing, String}} - set_pkgorigin_version_path(modkey, modpath) + modspec, modkey, modbuild_id, modcachepath, modstaledeps, modocachepath = dep::Tuple{PkgLoadSpec, PkgId, UInt128, String, Vector{Any}, Union{Nothing, String}} + set_pkgorigin_version_path(modkey, modspec.path) dep = _include_from_serialized(modkey, modcachepath, modocachepath, modstaledeps; register = stalecheck) if !isa(dep, Module) @debug "Rejecting cache file $path_to_try because required dependency $modkey failed to load from cache file for $modcachepath." exception=dep @@ -2179,10 +2256,10 @@ end for i in startedloading:length(staledeps) dep = staledeps[i] dep isa Module && continue - if dep isa Tuple{String, PkgId, UInt128} + if dep isa Tuple{PkgLoadSpec, PkgId, UInt128} _, modkey, _ = dep else - _, modkey, _ = dep::Tuple{String, PkgId, UInt128, String, Vector{Any}, Union{Nothing, String}} + _, modkey, _ = dep::Tuple{PkgLoadSpec, PkgId, UInt128, String, Vector{Any}, Union{Nothing, String}} end end_loading(modkey, nothing) end @@ -2651,6 +2728,11 @@ register_root_module(Main) # to the loaded_modules table instead of getting bindings. baremodule __toplevel__ using Base +global _internal_julia_parse = Core._parse +global _internal_julia_lower = Core._lower + +# Used for version checking of precompiled cache files only +global _internal_syntax_version::UInt8 = 0 end # get a top-level Module from the given key @@ -2699,13 +2781,15 @@ function __require_prelocked(pkg::PkgId, env) assert_havelock(require_lock) # perform the search operation to select the module file require intends to load - path = locate_package(pkg, env) - if path === nothing + specenv = locate_package_env(pkg, env) + if specenv === nothing throw(ArgumentError(""" Package $(repr("text/plain", pkg)) is required but does not seem to be installed: - Run `Pkg.instantiate()` to install all recorded dependencies. """)) end + spec = specenv[1] + path = spec.path set_pkgorigin_version_path(pkg, path) parallel_precompile_attempted = false # being safe to avoid getting stuck in a precompilepkgs loop @@ -2713,7 +2797,7 @@ function __require_prelocked(pkg::PkgId, env) # attempt to load the module file via the precompile cache locations if JLOptions().use_compiled_modules != 0 @label load_from_cache - loaded = _require_search_from_serialized(pkg, path, UInt128(0), true; reasons) + loaded = _require_search_from_serialized(pkg, spec, UInt128(0), true; reasons) if loaded isa Module return loaded end @@ -2740,10 +2824,10 @@ function __require_prelocked(pkg::PkgId, env) if !generating_output(#=incremental=#false) project = active_project() # spawn off a new incremental pre-compile task for recursive `require` calls - loaded = let path = path, reasons = reasons - maybe_cachefile_lock(pkg, path) do + loaded = let spec = spec, reasons = reasons + maybe_cachefile_lock(pkg, spec.path) do # double-check the search now that we have lock - m = _require_search_from_serialized(pkg, path, UInt128(0), true) + m = _require_search_from_serialized(pkg, spec, UInt128(0), true) m isa Module && return m verbosity = isinteractive() ? CoreLogging.Info : CoreLogging.Debug @@ -2777,7 +2861,7 @@ function __require_prelocked(pkg::PkgId, env) end end end - return compilecache(pkg, path; loadable_exts) + return compilecache(pkg, spec; loadable_exts) finally lock(require_lock) end @@ -2818,11 +2902,13 @@ function __require_prelocked(pkg::PkgId, env) if uuid !== old_uuid ccall(:jl_set_module_uuid, Cvoid, (Any, NTuple{2, UInt64}), __toplevel__, uuid) end + __toplevel__._internal_julia_parse = Experimental.VersionedParse(spec.julia_syntax_version) unlock(require_lock) try include(__toplevel__, path) loaded = maybe_root_module(pkg) finally + __toplevel__._internal_julia_parse = Core._parse lock(require_lock) if uuid !== old_uuid ccall(:jl_set_module_uuid, Cvoid, (Any, NTuple{2, UInt64}), __toplevel__, old_uuid) @@ -2920,7 +3006,7 @@ function require_stdlib(package_uuidkey::PkgId, ext::Union{Nothing, String}, fro sourcepath = find_ext_path(normpath(joinpath(env, package_uuidkey.name)), ext) end set_pkgorigin_version_path(this_uuidkey, sourcepath) - newm = _require_search_from_serialized(this_uuidkey, sourcepath, UInt128(0), false; DEPOT_PATH=depot_path) + newm = _require_search_from_serialized(this_uuidkey, PkgLoadSpec(sourcepath, VERSION), UInt128(0), false; DEPOT_PATH=depot_path) end finally end_loading(this_uuidkey, newm) @@ -2957,7 +3043,7 @@ function include_string(mapexpr::Function, mod::Module, code::AbstractString, filename::AbstractString="string") loc = LineNumberNode(1, Symbol(filename)) try - ast = Meta.parseall(code, filename=filename) + ast = Meta.parseall(code; filename, mod) if !Meta.isexpr(ast, :toplevel) @assert Core._lower != fl_lower # Only reached when JuliaLowering and alternate parse functions are activated @@ -3126,7 +3212,7 @@ end const newly_inferred = CodeInstance[] # this is called in the external process that generates precompiled package files -function include_package_for_output(pkg::PkgId, input::String, depot_path::Vector{String}, dl_load_path::Vector{String}, load_path::Vector{String}, +function include_package_for_output(pkg::PkgId, input::String, syntax_version::VersionNumber, depot_path::Vector{String}, dl_load_path::Vector{String}, load_path::Vector{String}, concrete_deps::typeof(_concrete_dependencies), source::Union{Nothing,String}) @lock require_lock begin @@ -3150,6 +3236,10 @@ function include_package_for_output(pkg::PkgId, input::String, depot_path::Vecto end ccall(:jl_set_newly_inferred, Cvoid, (Any,), newly_inferred) + # This one changes the parser behavior + __toplevel__._internal_julia_parse = Experimental.VersionedParse(syntax_version) + # This one is the compatibility marker for cache loading + __toplevel__._internal_syntax_version = cache_syntax_version(syntax_version) try Base.include(Base.__toplevel__, input) catch ex @@ -3185,7 +3275,7 @@ _pkg_str(_pkg::Pair{PkgId}) = _pkg_str(_pkg.first) * " => " * repr(_pkg.second) _pkg_str(_pkg::Nothing) = "nothing" const PRECOMPILE_TRACE_COMPILE = Ref{String}() -function create_expr_cache(pkg::PkgId, input::String, output::String, output_o::Union{Nothing, String}, +function create_expr_cache(pkg::PkgId, input::PkgLoadSpec, output::String, output_o::Union{Nothing, String}, concrete_deps::typeof(_concrete_dependencies), flags::Cmd=``, cacheflags::CacheFlags=CacheFlags(), internal_stderr::IO = stderr, internal_stdout::IO = stdout, loadable_exts::Union{Vector{PkgId},Nothing}=nothing) @nospecialize internal_stderr internal_stdout @@ -3251,7 +3341,7 @@ function create_expr_cache(pkg::PkgId, input::String, output::String, output_o:: Base.track_nested_precomp($(_pkg_str(vcat(Base.precompilation_stack, pkg)))) Base.loadable_extensions = $(_pkg_str(loadable_exts)) Base.precompiling_extension = $(loading_extension) - Base.include_package_for_output($(_pkg_str(pkg)), $(repr(abspath(input))), $(repr(depot_path)), $(repr(dl_load_path)), + Base.include_package_for_output($(_pkg_str(pkg)), $(repr(abspath(input.path))), $(repr(input.julia_syntax_version)), $(repr(depot_path)), $(repr(dl_load_path)), $(repr(load_path)), $(_pkg_str(concrete_deps)), $(repr(source_path(nothing)))) """) close(io.in) @@ -3309,14 +3399,14 @@ for important notes. """ function compilecache(pkg::PkgId, internal_stderr::IO = stderr, internal_stdout::IO = stdout; flags::Cmd=``, cacheflags::CacheFlags=CacheFlags(), loadable_exts::Union{Vector{PkgId},Nothing}=nothing) @nospecialize internal_stderr internal_stdout - path = locate_package(pkg) - path === nothing && throw(ArgumentError("$(repr("text/plain", pkg)) not found during precompilation")) - return compilecache(pkg, path, internal_stderr, internal_stdout; flags, cacheflags, loadable_exts) + spec = locate_package_load_spec(pkg) + spec === nothing && throw(ArgumentError("$(repr("text/plain", pkg)) not found during precompilation")) + return compilecache(pkg, spec, internal_stderr, internal_stdout; flags, cacheflags, loadable_exts) end const MAX_NUM_PRECOMPILE_FILES = Ref(10) -function compilecache(pkg::PkgId, path::String, internal_stderr::IO = stderr, internal_stdout::IO = stdout, +function compilecache(pkg::PkgId, spec::PkgLoadSpec, internal_stderr::IO = stderr, internal_stdout::IO = stdout, keep_loaded_modules::Bool = true; flags::Cmd=``, cacheflags::CacheFlags=CacheFlags(), loadable_exts::Union{Vector{PkgId},Nothing}=nothing) @@ -3356,7 +3446,7 @@ function compilecache(pkg::PkgId, path::String, internal_stderr::IO = stderr, in close(tmpio_o) close(tmpio_so) end - p = create_expr_cache(pkg, path, tmppath, tmppath_o, concrete_deps, flags, cacheflags, internal_stderr, internal_stdout, loadable_exts) + p = create_expr_cache(pkg, spec, tmppath, tmppath_o, concrete_deps, flags, cacheflags, internal_stderr, internal_stdout, loadable_exts) if success(p) if cache_objects @@ -3388,7 +3478,7 @@ function compilecache(pkg::PkgId, path::String, internal_stderr::IO = stderr, in end # inherit permission from the source file (and make them writable) - chmod(tmppath, filemode(path) & 0o777 | 0o200) + chmod(tmppath, filemode(spec.path) & 0o777 | 0o200) # prune the directory with cache files if pkg.uuid !== nothing @@ -3553,6 +3643,7 @@ end function _parse_cache_header(f::IO, cachefile::AbstractString) flags = read(f, UInt8) + syntax_version = read(f, UInt8) modules = read_module_list(f, false) totbytes = Int64(read(f, UInt64)) # total bytes for file dependencies + preferences # read the list of requirements @@ -3618,12 +3709,12 @@ function _parse_cache_header(f::IO, cachefile::AbstractString) srcfiles = srctext_files(f, srctextpos, includes) - return modules, (includes, srcfiles, requires), required_modules, srctextpos, prefs, prefs_hash, clone_targets, flags + return modules, (includes, srcfiles, requires), required_modules, srctextpos, prefs, prefs_hash, clone_targets, flags, syntax_version end function parse_cache_header(f::IO, cachefile::AbstractString) modules, (includes, srcfiles, requires), required_modules, - srctextpos, prefs, prefs_hash, clone_targets, flags = _parse_cache_header(f, cachefile) + srctextpos, prefs, prefs_hash, clone_targets, flags, syntax_version = _parse_cache_header(f, cachefile) includes_srcfiles = CacheHeaderIncludes[] includes_depfiles = CacheHeaderIncludes[] @@ -3697,7 +3788,7 @@ function parse_cache_header(f::IO, cachefile::AbstractString) end end - return modules, (includes, includes_srcfiles, requires), required_modules, srctextpos, prefs, prefs_hash, clone_targets, flags + return modules, (includes, includes_srcfiles, requires), required_modules, srctextpos, prefs, prefs_hash, clone_targets, flags, syntax_version end function parse_cache_header(cachefile::String) @@ -4087,12 +4178,19 @@ function any_includes_stale(includes::Vector{CacheHeaderIncludes}, cachefile::St return false end +function cache_syntax_version(ver::VersionNumber) + UInt8(clamp(ver.minor - 13, 0, 255)) +end + # returns true if it "cachefile.ji" is stale relative to "modpath.jl" and build_id for modkey # otherwise returns the list of dependencies to also check -@constprop :none function stale_cachefile(modpath::String, cachefile::String; ignore_loaded::Bool = false, requested_flags::CacheFlags=CacheFlags(), reasons=nothing) - return stale_cachefile(PkgId(""), UInt128(0), modpath, cachefile; ignore_loaded, requested_flags, reasons) +@constprop :none function stale_cachefile(modpath::String, cachefile::String; kwargs...) + return stale_cachefile(PkgLoadSpec(modpath, VERSION), cachefile; kwargs...) end -@constprop :none function stale_cachefile(modkey::PkgId, build_id::UInt128, modpath::String, cachefile::String; +@constprop :none function stale_cachefile(modspec::PkgLoadSpec, cachefile::String; ignore_loaded::Bool = false, requested_flags::CacheFlags=CacheFlags(), reasons=nothing) + return stale_cachefile(PkgId(""), UInt128(0), modspec, cachefile; ignore_loaded, requested_flags, reasons) +end +@constprop :none function stale_cachefile(modkey::PkgId, build_id::UInt128, modspec::PkgLoadSpec, cachefile::String; ignore_loaded::Bool=false, requested_flags::CacheFlags=CacheFlags(), reasons::Union{Dict{String,Int},Nothing}=nothing, stalecheck::Bool=true) # n.b.: this function does nearly all of the file validation, not just those checks related to stale, so the name is potentially unclear @@ -4110,7 +4208,7 @@ end record_reason(reasons, "different Julia build configuration") return true # incompatible cache file end - modules, (includes, _, requires), required_modules, srctextpos, prefs, prefs_hash, clone_targets, actual_flags = parse_cache_header(io, cachefile) + modules, (includes, _, requires), required_modules, srctextpos, prefs, prefs_hash, clone_targets, actual_flags, syntax_version = parse_cache_header(io, cachefile) if isempty(modules) return true # ignore empty file end @@ -4123,6 +4221,11 @@ end record_reason(reasons, "different compilation options") return true end + if syntax_version != cache_syntax_version(modspec.julia_syntax_version) + @debug "Rejecting cache file $cachefile for $modkey since it was parsed for a different Julia syntax version" + record_reason(reasons, "different Julia syntax version") + return true + end pkgimage = !isempty(clone_targets) if pkgimage ocachefile = ocachefile_from_cachefile(cachefile) @@ -4196,13 +4299,13 @@ end return true # Won't be able to fulfill dependency end end - path = locate_package(req_key) # TODO: add env and/or skip this when stalecheck is false - if path === nothing + spec = locate_package_load_spec(req_key) # TODO: add env and/or skip this when stalecheck is false + if spec === nothing @debug "Rejecting cache file $cachefile because dependency $req_key not found." record_reason(reasons, "dependency source file not found") return true # Won't be able to fulfill dependency end - depmods[i] = (path, req_key, req_build_id) + depmods[i] = (spec, req_key, req_build_id) end # check if this file is going to provide one of our concrete dependencies @@ -4226,12 +4329,12 @@ end # now check if this file's content hash has changed relative to its source files if stalecheck - if !samefile(includes[1].filename, modpath) + if !samefile(includes[1].filename, modspec.path) # In certain cases the path rewritten by `fixup_stdlib_path` may # point to an unreadable directory, make sure we can `stat` the # file before comparing it with `modpath`. stdlib_path = fixup_stdlib_path(includes[1].filename) - if !(isreadable(stdlib_path) && samefile(stdlib_path, modpath)) + if !(isreadable(stdlib_path) && samefile(stdlib_path, modspec.path)) @debug "Rejecting cache file $cachefile because it is for file $(includes[1].filename) not file $modpath" record_reason(reasons, "different source file path") return true # cache file was compiled from a different path @@ -4391,7 +4494,7 @@ function precompile(@nospecialize(argt::Type), m::Method) return precompile(mi) end -precompile(include_package_for_output, (PkgId, String, Vector{String}, Vector{String}, Vector{String}, typeof(_concrete_dependencies), Nothing)) || @assert false -precompile(include_package_for_output, (PkgId, String, Vector{String}, Vector{String}, Vector{String}, typeof(_concrete_dependencies), String)) || @assert false -precompile(create_expr_cache, (PkgId, String, String, String, typeof(_concrete_dependencies), Cmd, CacheFlags, IO, IO)) || @assert false -precompile(create_expr_cache, (PkgId, String, String, Nothing, typeof(_concrete_dependencies), Cmd, CacheFlags, IO, IO)) || @assert false +precompile(include_package_for_output, (PkgId, String, VersionNumber, Vector{String}, Vector{String}, Vector{String}, typeof(_concrete_dependencies), Nothing)) || @assert false +precompile(include_package_for_output, (PkgId, String, VersionNumber, Vector{String}, Vector{String}, Vector{String}, typeof(_concrete_dependencies), String)) || @assert false +precompile(create_expr_cache, (PkgId, PkgLoadSpec, String, String, typeof(_concrete_dependencies), Cmd, CacheFlags, IO, IO)) || @assert false +precompile(create_expr_cache, (PkgId, PkgLoadSpec, String, Nothing, typeof(_concrete_dependencies), Cmd, CacheFlags, IO, IO)) || @assert false diff --git a/base/meta.jl b/base/meta.jl index fa07c9582cd0c..1f5f49b72575f 100644 --- a/base/meta.jl +++ b/base/meta.jl @@ -304,12 +304,21 @@ end ParseError(msg::AbstractString) = ParseError(msg, nothing) +# N.B.: Should match definition in src/ast.c:jl_parse +function parser_for_module(mod::Union{Module, Nothing}) + mod === nothing && return Core._parse + isdefined(mod, :_internal_julia_parse) ? + getglobal(mod, :_internal_julia_parse) : + Core._parse +end + function _parse_string(text::AbstractString, filename::AbstractString, - lineno::Integer, index::Integer, options) + lineno::Integer, index::Integer, options, + _parse=parser_for_module(nothing)) if index < 1 || index > ncodeunits(text) + 1 throw(BoundsError(text, index)) end - ex, offset::Int = Core._parse(text, filename, lineno, index-1, options) + ex, offset::Int = _parse(text, filename, lineno, index-1, options) ex, offset+1 end @@ -346,8 +355,8 @@ julia> Meta.parse("(α, β) = 3, 5", 11, greedy=false) ``` """ function parse(str::AbstractString, pos::Integer; - filename="none", greedy::Bool=true, raise::Bool=true, depwarn::Bool=true) - ex, pos = _parse_string(str, String(filename), 1, pos, greedy ? :statement : :atom) + filename="none", greedy::Bool=true, raise::Bool=true, depwarn::Bool=true, mod = nothing) + ex, pos = _parse_string(str, String(filename), 1, pos, greedy ? :statement : :atom, parser_for_module(mod)) if raise && isexpr(ex, :error) err = ex.args[1] if err isa String @@ -386,8 +395,8 @@ julia> Meta.parse("x = ") ``` """ function parse(str::AbstractString; - filename="none", raise::Bool=true, depwarn::Bool=true) - ex, pos = parse(str, 1; filename, greedy=true, raise, depwarn) + filename="none", raise::Bool=true, depwarn::Bool=true, mod = nothing) + ex, pos = parse(str, 1; filename, greedy=true, raise, depwarn, mod = mod) if isexpr(ex, :error) return ex end @@ -398,12 +407,12 @@ function parse(str::AbstractString; return ex end -function parseatom(text::AbstractString, pos::Integer; filename="none", lineno=1) - return _parse_string(text, String(filename), lineno, pos, :atom) +function parseatom(text::AbstractString, pos::Integer; filename="none", lineno=1, mod = nothing) + return _parse_string(text, String(filename), lineno, pos, :atom, parser_for_module(mod)) end -function parseall(text::AbstractString; filename="none", lineno=1) - ex,_ = _parse_string(text, String(filename), lineno, 1, :all) +function parseall(text::AbstractString; filename="none", lineno=1, mod = nothing) + ex,_ = _parse_string(text, String(filename), lineno, 1, :all, parser_for_module(mod)) return ex end diff --git a/base/precompilation.jl b/base/precompilation.jl index 60ac3e3aa0f2c..52ffdb3fb69bd 100644 --- a/base/precompilation.jl +++ b/base/precompilation.jl @@ -1058,19 +1058,19 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, cachepaths = Base.find_all_in_cache_path(pkg) freshpaths = String[] cachepath_cache[pkg] = freshpaths - sourcepath = Base.locate_package(pkg) + sourcespec = Base.locate_package_load_spec(pkg) single_requested_pkg = length(requested_pkgs) == 1 && (pkg in requested_pkgids || pkg.name in pkg_names) for config in configs pkg_config = (pkg, config) - if sourcepath === nothing + if sourcespec === nothing failed_deps[pkg_config] = "Error: Missing source file for $(pkg)" notify(was_processed[pkg_config]) continue end # Heuristic for when precompilation is disabled, which must not over-estimate however for any dependent # since it will also block precompilation of all dependents - if _from_loading && single_requested_pkg && occursin(r"\b__precompile__\(\s*false\s*\)", read(sourcepath, String)) + if _from_loading && single_requested_pkg && occursin(r"\b__precompile__\(\s*false\s*\)", read(sourcespec.path, String)) @lock print_lock begin Base.@logmsg logcalls "Disabled precompiling $(repr("text/plain", pkg)) since the text `__precompile__(false)` was found in file." end @@ -1088,7 +1088,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, end end circular = pkg in circular_deps - freshpath = Base.compilecache_freshest_path(pkg; ignore_loaded, stale_cache, cachepath_cache, cachepaths, sourcepath, flags=cacheflags) + freshpath = Base.compilecache_freshest_path(pkg; ignore_loaded, stale_cache, cachepath_cache, cachepaths, sourcespec, flags=cacheflags) is_stale = freshpath === nothing if !is_stale push!(freshpaths, freshpath) @@ -1102,6 +1102,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, std_pipe = Base.link_pipe!(Pipe(); reader_supports_async=true, writer_supports_async=true) t_monitor = @async monitor_std(pkg_config, std_pipe; single_requested_pkg) + local name try name = describe_pkg(pkg, is_project_dep, is_serial_dep, flags, cacheflags) @lock print_lock begin @@ -1126,7 +1127,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, if _from_loading && pkg in requested_pkgids # loading already took the cachefile_lock and printed logmsg for its explicit requests t = @elapsed ret = begin - Base.compilecache(pkg, sourcepath, std_pipe, std_pipe, !ignore_loaded; + Base.compilecache(pkg, sourcespec, std_pipe, std_pipe, !ignore_loaded; flags, cacheflags, loadable_exts) end else @@ -1138,7 +1139,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, return ErrorException("canceled") end cachepaths = Base.find_all_in_cache_path(pkg) - local freshpath = Base.compilecache_freshest_path(pkg; ignore_loaded, stale_cache, cachepath_cache, cachepaths, sourcepath, flags=cacheflags) + local freshpath = Base.compilecache_freshest_path(pkg; ignore_loaded, stale_cache, cachepath_cache, cachepaths, sourcespec, flags=cacheflags) local is_stale = freshpath === nothing if !is_stale push!(freshpaths, freshpath) @@ -1147,7 +1148,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, logcalls === CoreLogging.Debug && @lock print_lock begin @debug "Precompiling $(repr("text/plain", pkg))" end - Base.compilecache(pkg, sourcepath, std_pipe, std_pipe, !ignore_loaded; + Base.compilecache(pkg, sourcespec, std_pipe, std_pipe, !ignore_loaded; flags, cacheflags, loadable_exts) end end @@ -1165,7 +1166,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, cachefile, _ = ret::Tuple{String, Union{Nothing, String}} push!(freshpaths, cachefile) build_id, _ = Base.parse_cache_buildid(cachefile) - stale_cache_key = (pkg, build_id, sourcepath, cachefile, ignore_loaded, cacheflags)::StaleCacheKey + stale_cache_key = (pkg, build_id, sourcespec, cachefile, ignore_loaded, cacheflags)::StaleCacheKey stale_cache[stale_cache_key] = false end end @@ -1193,8 +1194,8 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, notify(was_processed[pkg_config]) catch err_outer # For debugging: - # println("Task failed $err_outer") - # Base.display_error(ErrorException(""), Base.catch_backtrace())# logging doesn't show here + println("Task failed $err_outer") + Base.display_error(ErrorException(""), Base.catch_backtrace())# logging doesn't show here handle_interrupt(err_outer, false) rethrow() end @@ -1207,8 +1208,8 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, interrupted_or_done[] = true catch err # For debugging: - # println("Task failed $err") - # Base.display_error(ErrorException(""), Base.catch_backtrace())# logging doesn't show here + println("Task failed $err") + Base.display_error(ErrorException(""), Base.catch_backtrace())# logging doesn't show here handle_interrupt(err, false) || rethrow() finally try diff --git a/base/runtime_internals.jl b/base/runtime_internals.jl index 25e174a14d204..5cb0f60966618 100644 --- a/base/runtime_internals.jl +++ b/base/runtime_internals.jl @@ -1800,9 +1800,6 @@ hasintersect(@nospecialize(a), @nospecialize(b)) = typeintersect(a, b) !== Botto # scoping # ########### -_topmod(m::Module) = ccall(:jl_base_relative_to, Any, (Any,), m)::Module - - # high-level, more convenient method lookup functions function visit(f, mt::Core.MethodTable) diff --git a/base/version.jl b/base/version.jl index 71290bc95b769..5870b11c5ac18 100644 --- a/base/version.jl +++ b/base/version.jl @@ -275,3 +275,470 @@ else end libllvm_path() = ccall(:jl_get_libllvm, Any, ()) + + +################ +# VersionBound # +################ +struct VersionBound + t::NTuple{3, UInt32} + n::Int + function VersionBound(tin::NTuple{n, Integer}) where {n} + n <= 3 || throw(ArgumentError("VersionBound: you can only specify major, minor and patch versions")) + n == 0 && return new((0, 0, 0), n) + n == 1 && return new((tin[1], 0, 0), n) + n == 2 && return new((tin[1], tin[2], 0), n) + n == 3 && return new((tin[1], tin[2], tin[3]), n) + error("invalid $n") + end +end +VersionBound(t::Integer...) = VersionBound(t) +VersionBound(v::VersionNumber) = VersionBound(v.major, v.minor, v.patch) + +Base.getindex(b::VersionBound, i::Int) = b.t[i] + +function ≲(v::VersionNumber, b::VersionBound) + b.n == 0 && return true + b.n == 1 && return v.major <= b[1] + b.n == 2 && return (v.major, v.minor) <= (b[1], b[2]) + return (v.major, v.minor, v.patch) <= (b[1], b[2], b[3]) +end + +function ≲(b::VersionBound, v::VersionNumber) + b.n == 0 && return true + b.n == 1 && return v.major >= b[1] + b.n == 2 && return (v.major, v.minor) >= (b[1], b[2]) + return (v.major, v.minor, v.patch) >= (b[1], b[2], b[3]) +end + +function isless_ll(a::VersionBound, b::VersionBound) + m, n = a.n, b.n + for i in 1:min(m, n) + a[i] < b[i] && return true + a[i] > b[i] && return false + end + return m < n +end + +stricterlower(a::VersionBound, b::VersionBound) = isless_ll(a, b) ? b : a + +# Comparison between two upper bounds +function isless_uu(a::VersionBound, b::VersionBound) + m, n = a.n, b.n + for i in 1:min(m, n) + a[i] < b[i] && return true + a[i] > b[i] && return false + end + return m > n +end + +stricterupper(a::VersionBound, b::VersionBound) = isless_uu(a, b) ? a : b + +# `isjoinable` compares an upper bound of a range with the lower bound of the next range +# to determine if they can be joined, as in [1.5-2.8, 2.5-3] -> [1.5-3]. Used by `union!`. +# The equal-length-bounds case is special since e.g. `1.5` can be joined with `1.6`, +# `2.3.4` can be joined with `2.3.5` etc. + +function isjoinable(up::VersionBound, lo::VersionBound) + up.n == 0 && lo.n == 0 && return true + if up.n == lo.n + n = up.n + for i in 1:(n - 1) + up[i] > lo[i] && return true + up[i] < lo[i] && return false + end + up[n] < lo[n] - 1 && return false + return true + else + l = min(up.n, lo.n) + for i in 1:l + up[i] > lo[i] && return true + up[i] < lo[i] && return false + end + end + return true +end + +Base.hash(r::VersionBound, h::UInt) = hash(r.t, hash(r.n, h)) + +# Hot code +function VersionBound(s::AbstractString) + s = strip(s) + s == "*" && return VersionBound() + first(s) == 'v' && (s = SubString(s, 2)) + l = lastindex(s) + + p = findnext('.', s, 1) + b = p === nothing ? l : (p - 1) + i = parse(Int64, SubString(s, 1, b)) + p === nothing && return VersionBound(i) + + a = p + 1 + p = findnext('.', s, a) + b = p === nothing ? l : (p - 1) + j = parse(Int64, SubString(s, a, b)) + p === nothing && return VersionBound(i, j) + + a = p + 1 + p = findnext('.', s, a) + b = p === nothing ? l : (p - 1) + k = parse(Int64, SubString(s, a, b)) + p === nothing && return VersionBound(i, j, k) + + error("invalid VersionBound string $(repr(s))") +end + +################ +# VersionRange # +################ +struct VersionRange + lower::VersionBound + upper::VersionBound + # NOTE: ranges are allowed to be empty; they are ignored by VersionSpec anyway + function VersionRange(lo::VersionBound, hi::VersionBound) + # lo.t == hi.t implies that digits past min(lo.n, hi.n) are zero + # lo.n < hi.n example: 1.2-1.2.0 => 1.2.0 + # lo.n > hi.n example: 1.2.0-1.2 => 1.2 + lo.t == hi.t && (lo = hi) + return new(lo, hi) + end +end +VersionRange(b::VersionBound = VersionBound()) = VersionRange(b, b) +VersionRange(t::Integer...) = VersionRange(VersionBound(t...)) +VersionRange(v::VersionNumber) = VersionRange(VersionBound(v)) +VersionRange(lo::VersionNumber, hi::VersionNumber) = VersionRange(VersionBound(lo), VersionBound(hi)) + +# The vast majority of VersionRanges are in practice equal to "1" +const VersionRange_1 = VersionRange(VersionBound("1"), VersionBound("1")) +function VersionRange(s::AbstractString) + s == "1" && return VersionRange_1 + p = split(s, "-") + if length(p) != 1 && length(p) != 2 + throw(ArgumentError("invalid version range: $(repr(s))")) + end + lower = VersionBound(p[1]) + upper = length(p) == 1 ? lower : VersionBound(p[2]) + return VersionRange(lower, upper) +end + +function Base.isempty(r::VersionRange) + for i in 1:min(r.lower.n, r.upper.n) + r.lower[i] > r.upper[i] && return true + r.lower[i] < r.upper[i] && return false + end + return false +end + +function Base.print(io::IO, r::VersionRange) + m, n = r.lower.n, r.upper.n + return if (m, n) == (0, 0) + print(io, '*') + elseif m == 0 + print(io, "0 -") + join(io, r.upper.t, '.') + elseif n == 0 + join(io, r.lower.t, '.') + print(io, " - *") + else + join(io, r.lower.t[1:m], '.') + if r.lower != r.upper + print(io, " - ") + join(io, r.upper.t[1:n], '.') + end + end +end +Base.show(io::IO, r::VersionRange) = print(io, "VersionRange(\"", r, "\")") + +Base.in(v::VersionNumber, r::VersionRange) = r.lower ≲ v ≲ r.upper + +Base.intersect(a::VersionRange, b::VersionRange) = VersionRange(stricterlower(a.lower, b.lower), stricterupper(a.upper, b.upper)) + +function Base.union!(ranges::Vector{<:VersionRange}) + l = length(ranges) + l == 0 && return ranges + + sort!(ranges, lt = (a, b) -> (isless_ll(a.lower, b.lower) || (a.lower == b.lower && isless_uu(a.upper, b.upper)))) + + k0 = 1 + ks = findfirst(!isempty, ranges) + ks === nothing && return empty!(ranges) + + lo, up, k0 = ranges[ks].lower, ranges[ks].upper, 1 + for k in (ks + 1):l + isempty(ranges[k]) && continue + lo1, up1 = ranges[k].lower, ranges[k].upper + if isjoinable(up, lo1) + isless_uu(up, up1) && (up = up1) + continue + end + vr = VersionRange(lo, up) + @assert !isempty(vr) + ranges[k0] = vr + k0 += 1 + lo, up = lo1, up1 + end + vr = VersionRange(lo, up) + if !isempty(vr) + ranges[k0] = vr + k0 += 1 + end + resize!(ranges, k0 - 1) + return ranges +end + +Base.minimum(r::VersionRange) = r.lower + +############### +# VersionSpec # +############### +struct VersionSpec + ranges::Vector{VersionRange} + VersionSpec(r::Vector{<:VersionRange}) = new(length(r) == 1 ? r : union!(r)) + VersionSpec(vs::VersionSpec) = vs +end + +VersionSpec(r::VersionRange) = VersionSpec(VersionRange[r]) +VersionSpec(v::VersionNumber) = VersionSpec(VersionRange(v)) +const _all_versionsspec = VersionSpec(VersionRange()) +VersionSpec() = _all_versionsspec +VersionSpec(s::AbstractString) = VersionSpec(VersionRange(s)) +VersionSpec(v::AbstractVector) = VersionSpec(map(VersionRange, v)) + +# Hot code +function Base.in(v::VersionNumber, s::VersionSpec) + for r in s.ranges + v in r && return true + end + return false +end + +# Optimized batch version check for version lists +# Fills dest[1:n] indicating which versions are in the VersionSpec +# Optimized for sorted version lists (but works correctly even if unsorted) +# Note: Only fills indices 1:n, leaves rest of dest unchanged +function matches_spec_range!(dest::BitVector, versions::AbstractVector{VersionNumber}, spec::VersionSpec, n::Int) + @assert length(versions) == n + @assert length(dest) >= n + + # Initialize to false + dest[1:n] .= false + + isempty(spec.ranges) && return dest + + # Assumes versions are sorted (as created in Operations.jl:1002) + # If sorted, this avoids O(n*m) comparisons by scanning linearly + @inbounds for range in spec.ranges + # Find first version that could be in range + i = 1 + while i <= n && !(range.lower ≲ versions[i]) + i += 1 + end + + # Mark all versions in range + while i <= n && versions[i] ≲ range.upper + dest[i] = true + i += 1 + end + end + + return dest +end + +Base.copy(vs::VersionSpec) = VersionSpec(vs) + +const empty_versionspec = VersionSpec(VersionRange[]) +const _empty_symbol = "∅" + +Base.isempty(s::VersionSpec) = all(isempty, s.ranges) +@assert isempty(empty_versionspec) +# Hot code, measure performance before changing +function Base.intersect(A::VersionSpec, B::VersionSpec) + (isempty(A) || isempty(B)) && return copy(empty_versionspec) + ranges = Vector{VersionRange}(undef, length(A.ranges) * length(B.ranges)) + i = 1 + @inbounds for a in A.ranges, b in B.ranges + ranges[i] = intersect(a, b) + i += 1 + end + return VersionSpec(ranges) +end +Base.intersect(a::VersionNumber, B::VersionSpec) = a in B ? VersionSpec(a) : empty_versionspec +Base.intersect(A::VersionSpec, b::VersionNumber) = intersect(b, A) + +function Base.union(A::VersionSpec, B::VersionSpec) + A == B && return A + Ar = copy(A.ranges) + append!(Ar, B.ranges) + union!(Ar) + return VersionSpec(Ar) +end + +Base.:(==)(A::VersionSpec, B::VersionSpec) = A.ranges == B.ranges +Base.hash(s::VersionSpec, h::UInt) = hash(s.ranges, h + (0x2fd2ca6efa023f44 % UInt)) + +function Base.print(io::IO, s::VersionSpec) + isempty(s) && return print(io, _empty_symbol) + length(s.ranges) == 1 && return print(io, s.ranges[1]) + print(io, '[') + for i in 1:length(s.ranges) + 1 < i && print(io, ", ") + print(io, s.ranges[i]) + end + return print(io, ']') +end + +function Base.show(io::IO, s::VersionSpec) + print(io, "VersionSpec(") + if length(s.ranges) == 1 + print(io, '"', s.ranges[1], '"') + else + print(io, "[") + for i in 1:length(s.ranges) + 1 < i && print(io, ", ") + print(io, '"', s.ranges[i], '"') + end + print(io, ']') + end + return print(io, ")") +end + +Base.minimum(v::VersionSpec) = minimum(v.ranges[1]) + +################### +# Semver notation # +################### + +function semver_spec(s::String; throw = true) + ranges = VersionRange[] + for ver in strip.(split(strip(s), ',')) + range = nothing + found_match = false + for (ver_reg, f) in ver_regs + if occursin(ver_reg, ver) + range = f(match(ver_reg, ver)) + found_match = true + break + end + end + if !found_match + if throw + error("invalid version specifier: \"$s\"") + else + return nothing + end + end + push!(ranges, range) + end + return VersionSpec(ranges) +end + +function semver_interval(m::RegexMatch) + @assert length(m.captures) == 4 + n_significant = count(x -> x !== nothing, m.captures) - 1 + typ, _major, _minor, _patch = m.captures + major = parse(Int, _major) + minor = (n_significant < 2) ? 0 : parse(Int, _minor) + patch = (n_significant < 3) ? 0 : parse(Int, _patch) + if n_significant == 3 && major == 0 && minor == 0 && patch == 0 + error("invalid version: \"0.0.0\"") + end + # Default type is :caret + vertyp = (typ == "" || typ == "^") ? :caret : :tilde + v0 = VersionBound((major, minor, patch)) + return if vertyp === :caret + if major != 0 + return VersionRange(v0, VersionBound((v0[1],))) + elseif minor != 0 + return VersionRange(v0, VersionBound((v0[1], v0[2]))) + else + if n_significant == 1 + return VersionRange(v0, VersionBound((0,))) + elseif n_significant == 2 + return VersionRange(v0, VersionBound((0, 0))) + else + return VersionRange(v0, VersionBound((0, 0, v0[3]))) + end + end + else + if n_significant == 3 || n_significant == 2 + return VersionRange(v0, VersionBound((v0[1], v0[2]))) + else + return VersionRange(v0, VersionBound((v0[1],))) + end + end +end + +const _inf = VersionBound("*") +function inequality_interval(m::RegexMatch) + @assert length(m.captures) == 4 + typ, _major, _minor, _patch = m.captures + n_significant = count(x -> x !== nothing, m.captures) - 1 + major = parse(Int, _major) + minor = (n_significant < 2) ? 0 : parse(Int, _minor) + patch = (n_significant < 3) ? 0 : parse(Int, _patch) + if n_significant == 3 && major == 0 && minor == 0 && patch == 0 + error("invalid version: 0.0.0") + end + v = VersionBound(major, minor, patch) + if occursin(r"^<\s*$", typ) + nil = VersionBound(0, 0, 0) + if v[3] == 0 + if v[2] == 0 + v1 = VersionBound(v[1] - 1) + else + v1 = VersionBound(v[1], v[2] - 1) + end + else + v1 = VersionBound(v[1], v[2], v[3] - 1) + end + return VersionRange(nil, v1) + elseif occursin(r"^=\s*$", typ) + return VersionRange(v) + elseif occursin(r"^>=\s*$", typ) || occursin(r"^≥\s*$", typ) + return VersionRange(v, _inf) + else + error("invalid prefix $typ") + end +end + +function hyphen_interval(m::RegexMatch) + @assert length(m.captures) == 6 + _lower_major, _lower_minor, _lower_patch, _upper_major, _upper_minor, _upper_patch = m.captures + if isnothing(_lower_minor) + lower_bound = VersionBound(parse(Int, _lower_major)) + elseif isnothing(_lower_patch) + lower_bound = VersionBound( + parse(Int, _lower_major), + parse(Int, _lower_minor) + ) + else + lower_bound = VersionBound( + parse(Int, _lower_major), + parse(Int, _lower_minor), + parse(Int, _lower_patch) + ) + end + if isnothing(_upper_minor) + upper_bound = VersionBound(parse(Int, _upper_major)) + elseif isnothing(_upper_patch) + upper_bound = VersionBound( + parse(Int, _upper_major), + parse(Int, _upper_minor) + ) + else + upper_bound = VersionBound( + parse(Int, _upper_major), + parse(Int, _upper_minor), + parse(Int, _upper_patch) + ) + end + return VersionRange(lower_bound, upper_bound) +end + +const version = "v?([0-9]+?)(?:\\.([0-9]+?))?(?:\\.([0-9]+?))?" +const ver_regs = + Pair{Regex, Any}[ + Regex("^([~^]?)?$version\$") => semver_interval, # 0.5 ^0.4 ~0.3.2 + Regex("^((?:≥\\s*)|(?:>=\\s*)|(?:=\\s*)|(?:<\\s*)|(?:=\\s*))v?$version\$") => inequality_interval, # < 0.2 >= 0.5,2 + Regex("^[\\s]*$version[\\s]*?\\s-\\s[\\s]*?$version[\\s]*\$") => hyphen_interval, # 0.7 - 1.3 +] diff --git a/doc/src/manual/code-loading.md b/doc/src/manual/code-loading.md index 5871530720d22..dd3658e555d2a 100644 --- a/doc/src/manual/code-loading.md +++ b/doc/src/manual/code-loading.md @@ -438,6 +438,32 @@ Preferences in environments higher up in the environment stack get overridden by This allows depot-wide preference defaults to exist, with active projects able to merge or even completely overwrite these inherited preferences. See the docstring for `Preferences.set_preferences!()` for the full details of how to set preferences to allow or disallow merging. +### [Syntax Versioning](@id syntax-versioning) + +Syntax versioning allows packages to specify which version of Julia's syntax they use. In particular, different +packages can use different versions of the Julia syntax. This allows evolution of Julia's syntax in a non-breaking +way, while allowing packages to upgrade at their own pace. The syntax version is determined from the package's +corresponding Project.toml and propagates to all modules defined in the package. + +#### Syntax Version Determination + +The syntax version for a package is determined by the loading mechanism in the following order of precedence: + +1. If a `syntax.julia_version` field is present in the project file, it is used directly: + ```toml + name = "MyPackage" + uuid = "..." + syntax.julia_version = "1.14" + ``` + +2. Otherwise, if a `[compat]` section specifies a Julia version constraint, the minimum compatible version is used: + ```toml + [compat] + julia = "1.13-2" # implies syntax version 1.13.0 + ``` + +3. If neither is specified, the current Julia version is used. + ## Conclusion Federated package management and precise software reproducibility are difficult but worthy goals in a package system. In combination, these goals lead to a more complex package loading mechanism than most dynamic languages have, but it also yields scalability and reproducibility that is more commonly associated with static languages. Typically, Julia users should be able to use the built-in package manager to manage their projects without needing a precise understanding of these interactions. A call to `Pkg.add("X")` will add to the appropriate project and manifest files, selected via `Pkg.activate("Y")`, so that a future call to `import X` will load `X` without further thought. diff --git a/src/ast.c b/src/ast.c index e8cdf57ad8194..d6e3893751c9f 100644 --- a/src/ast.c +++ b/src/ast.c @@ -1016,8 +1016,21 @@ static jl_value_t *jl_invoke_julia_macro(jl_array_t *args, jl_module_t *inmodule margs[0] = jl_array_ptr_ref(args, 0); // __source__ argument jl_value_t *lno = jl_array_ptr_ref(args, 1); - if (!jl_is_linenode(lno)) + jl_value_t *retry_lno = NULL; + if (!jl_is_linenumbernode(lno)) { + if (lno != jl_nothing) { + // Special case: The magic @VERSION macro currently gets a special + // Core.MacroSource for its __source__ argument. However, to avoid + // giving this to macros that do not expect it, we check for that + // special case and retry with just the LineNumberNode if needed. + if (jl_typeof(lno) == jl_get_global(jl_core_module, jl_symbol("MacroSource"))) { + retry_lno = jl_fieldref_noalloc(lno, 0); + goto lno_ok; + } + } lno = jl_new_struct(jl_linenumbernode_type, jl_box_long(0), jl_nothing); + } +lno_ok: margs[1] = lno; margs[2] = (jl_value_t*)inmodule; for (i = 3; i < nargs; i++) @@ -1029,9 +1042,17 @@ static jl_value_t *jl_invoke_julia_macro(jl_array_t *args, jl_module_t *inmodule ct->world_age = world; jl_value_t *result; JL_TRY { - margs[0] = jl_toplevel_eval(*ctx, margs[0]); - jl_method_instance_t *mfunc = jl_method_lookup(margs, nargs, ct->world_age); + jl_module_t *ctx_module = *ctx; + JL_GC_PROMISE_ROOTED(ctx_module); + margs[0] = jl_toplevel_eval(ctx_module, margs[0]); + jl_method_instance_t *mfunc = NULL; + mfunc = jl_method_lookup(margs, nargs, ct->world_age); JL_GC_PROMISE_ROOTED(mfunc); + if (mfunc == NULL && retry_lno != NULL) { + margs[1] = retry_lno; + mfunc = jl_method_lookup(margs, nargs, ct->world_age); + JL_GC_PROMISE_ROOTED(mfunc); + } if (mfunc == NULL) { jl_method_error(margs[0], &margs[1], nargs, ct->world_age); // unreachable @@ -1224,15 +1245,18 @@ JL_DLLEXPORT jl_value_t *jl_fl_lower(jl_value_t *expr, jl_module_t *inmodule, JL_DLLEXPORT jl_value_t *jl_lower(jl_value_t *expr, jl_module_t *inmodule, const char *filename, int line, size_t world, bool_t warn) { - jl_value_t *core_lower = NULL; - if (jl_core_module) - core_lower = jl_get_global_value(jl_core_module, jl_symbol("_lower"), jl_current_task->world_age); - if (!core_lower || core_lower == jl_nothing) { + jl_value_t *julia_lower = NULL; + if (inmodule) { + julia_lower = jl_get_global(inmodule, jl_symbol("_internal_julia_lower")); + } + if ((!julia_lower || julia_lower == jl_nothing) && jl_core_module) + julia_lower = jl_get_global_value(jl_core_module, jl_symbol("_lower"), jl_current_task->world_age); + if (!julia_lower || julia_lower == jl_nothing) { return jl_fl_lower(expr, inmodule, filename, line, world, warn); } jl_value_t **args; JL_GC_PUSHARGS(args, 7); - args[0] = core_lower; + args[0] = julia_lower; args[1] = expr; args[2] = (jl_value_t*)inmodule; args[3] = jl_cstr_to_string(filename); @@ -1288,20 +1312,23 @@ jl_code_info_t *jl_inner_ctor_body(jl_array_t *fieldkinds, jl_module_t *inmodule // `text` is passed as a pointer to allow raw non-String buffers to be used // without copying. jl_value_t *jl_parse(const char *text, size_t text_len, jl_value_t *filename, - size_t lineno, size_t offset, jl_value_t *options) + size_t lineno, size_t offset, jl_value_t *options, jl_module_t *inmodule) { - jl_value_t *core_parse = NULL; - if (jl_core_module) { - core_parse = jl_get_global(jl_core_module, jl_symbol("_parse")); + jl_value_t *parser = NULL; + if (inmodule) { + parser = jl_get_global(inmodule, jl_symbol("_internal_julia_parse")); + } + if ((!parser || parser == jl_nothing) && jl_core_module) { + parser = jl_get_global(jl_core_module, jl_symbol("_parse")); } - if (!core_parse || core_parse == jl_nothing) { + if (!parser || parser == jl_nothing) { // In bootstrap, directly call the builtin parser. jl_value_t *result = jl_fl_parse(text, text_len, filename, lineno, offset, options); return result; } jl_value_t **args; JL_GC_PUSHARGS(args, 6); - args[0] = core_parse; + args[0] = parser; args[1] = (jl_value_t*)jl_alloc_svec(2); jl_svecset(args[1], 0, jl_box_uint8pointer((uint8_t*)text)); jl_svecset(args[1], 1, jl_box_long(text_len)); @@ -1330,7 +1357,7 @@ JL_DLLEXPORT jl_value_t *jl_parse_all(const char *text, size_t text_len, { jl_value_t *fname = jl_pchar_to_string(filename, filename_len); JL_GC_PUSH1(&fname); - jl_value_t *p = jl_parse(text, text_len, fname, lineno, 0, (jl_value_t*)jl_all_sym); + jl_value_t *p = jl_parse(text, text_len, fname, lineno, 0, (jl_value_t*)jl_all_sym, NULL); JL_GC_POP(); return jl_svecref(p, 0); } @@ -1343,7 +1370,7 @@ JL_DLLEXPORT jl_value_t *jl_parse_string(const char *text, size_t text_len, jl_value_t *fname = jl_cstr_to_string("none"); JL_GC_PUSH1(&fname); jl_value_t *result = jl_parse(text, text_len, fname, 1, offset, - (jl_value_t*)(greedy ? jl_statement_sym : jl_atom_sym)); + (jl_value_t*)(greedy ? jl_statement_sym : jl_atom_sym), NULL); JL_GC_POP(); return result; } diff --git a/src/julia_internal.h b/src/julia_internal.h index d67a5dce810a2..487486ce29964 100644 --- a/src/julia_internal.h +++ b/src/julia_internal.h @@ -932,6 +932,7 @@ STATIC_INLINE size_t module_usings_max(jl_module_t *m) JL_NOTSAFEPOINT { } JL_DLLEXPORT jl_sym_t *jl_module_name(jl_module_t *m) JL_NOTSAFEPOINT; +jl_module_t *jl_module_root(jl_module_t *m); void jl_add_scanned_method(jl_module_t *m, jl_method_t *meth); jl_value_t *jl_eval_global_var(jl_module_t *m JL_PROPAGATES_ROOT, jl_sym_t *e, size_t world); 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 JL_DLLEXPORT int jl_has_meta(jl_array_t *body, jl_sym_t *sym) JL_NOTSAFEPOINT; JL_DLLEXPORT jl_value_t *jl_parse(const char *text, size_t text_len, jl_value_t *filename, - size_t lineno, size_t offset, jl_value_t *options); + size_t lineno, size_t offset, jl_value_t *options, jl_module_t *inmodule); jl_code_info_t *jl_inner_ctor_body(jl_array_t *fieldkinds, jl_module_t *inmodule, const char *file, int line); 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); void jl_ctor_def(jl_value_t *ty, jl_value_t *functionloc); diff --git a/src/staticdata.c b/src/staticdata.c index e225eb2fe10df..ee8fa97b7be36 100644 --- a/src/staticdata.c +++ b/src/staticdata.c @@ -3304,10 +3304,25 @@ static int ci_not_internal_cache(jl_code_instance_t *ci) return !(jl_atomic_load_relaxed(&ci->flags) & JL_CI_FLAGS_NATIVE_CACHE_VALID) || jl_object_in_image(mi->def.value); } +static uint8_t jl_get_toplevel_syntax_version(void) +{ + jl_task_t *ct = jl_current_task; + jl_module_t *toplevel = (jl_module_t*)jl_get_global_value(jl_base_module, jl_symbol("__toplevel__"), ct->world_age); + JL_GC_PROMISE_ROOTED(toplevel); + jl_value_t *syntax_version = jl_get_global_value(toplevel, jl_symbol("_internal_syntax_version"), ct->world_age); + return jl_unbox_uint8(syntax_version); +} + static void jl_write_header_for_incremental(ios_t *f, jl_array_t *worklist, jl_array_t *mod_array, jl_array_t **udeps, int64_t *srctextpos, int64_t *checksumpos) { *checksumpos = write_header(f, 0); write_uint8(f, jl_cache_flags()); + // write the syntax version marker. Note that unlike a VersionNumber, this is + // private to the serialization format and only needs to be reloaded by the + // same version of Julia that wrote it. As a result, we don't store the full + // VersionNumber, only an index of which of the supported syntax versions to + // select. + write_uint8(f, jl_get_toplevel_syntax_version()); // write description of contents (name, uuid, buildid) write_worklist_for_header(f, worklist); // Determine unique (module, abspath, fsize, hash, mtime) dependencies for the files defining modules in the worklist @@ -3359,6 +3374,7 @@ JL_DLLEXPORT void jl_create_system_image(void **_native_data, jl_array_t *workli if (emit_split) { checksumpos_ff = write_header(ff, 1); write_uint8(ff, jl_cache_flags()); + write_uint8(ff, jl_get_toplevel_syntax_version()); write_mod_list(ff, mod_array); } else { @@ -4305,6 +4321,8 @@ static jl_value_t *jl_validate_cache_file(ios_t *f, jl_array_t *depmods, uint64_ if (pkgimage && !jl_match_cache_flags_current(flags)) { return jl_get_exceptionf(jl_errorexception_type, "Pkgimage flags mismatch"); } + // Syntax version mismatch is not fatal to load + (void)read_uint8(f); // syntax_version if (!pkgimage) { // skip past the worklist size_t len; diff --git a/src/staticdata_utils.c b/src/staticdata_utils.c index 047db9af1cccf..16ebcd4b4ee12 100644 --- a/src/staticdata_utils.c +++ b/src/staticdata_utils.c @@ -652,7 +652,7 @@ static const char *jl_git_commit(void) // "magic" string and version header of .ji file -static const int JI_FORMAT_VERSION = 12; +static const int JI_FORMAT_VERSION = 13; static const char JI_MAGIC[] = "\373jli\r\n\032\n"; // based on PNG signature static const uint16_t BOM = 0xFEFF; // byte-order marker static int64_t write_header(ios_t *s, uint8_t pkgimage) diff --git a/src/timing.c b/src/timing.c index f0abd19a1e347..dd37bca3f5c35 100644 --- a/src/timing.c +++ b/src/timing.c @@ -10,8 +10,6 @@ #define DISABLE_FREQUENT_EVENTS #endif -jl_module_t *jl_module_root(jl_module_t *m); - #ifdef __cplusplus extern "C" { #endif diff --git a/src/toplevel.c b/src/toplevel.c index 4622a9e8b4ce0..b78ad6ec46b3c 100644 --- a/src/toplevel.c +++ b/src/toplevel.c @@ -111,7 +111,25 @@ static int jl_is__toplevel__mod(jl_module_t *mod, jl_task_t *ct) (jl_value_t*)mod == jl_get_global_value(jl_base_module, jl_symbol("__toplevel__"), ct->world_age); } -JL_DLLEXPORT jl_module_t *jl_begin_new_module(jl_module_t *parent_module, jl_sym_t *name, +JL_DLLEXPORT void jl_setup_new_module(jl_module_t *m, jl_value_t *syntax_version) +{ + jl_task_t *ct = jl_current_task; + size_t last_age = ct->world_age; + ct->world_age = jl_atomic_load_acquire(&jl_world_counter); + jl_value_t *f = jl_get_global_value(jl_base_module, jl_symbol("_setup_module!"), ct->world_age); + if (f != NULL) { + jl_value_t **fargs; + JL_GC_PUSHARGS(fargs, 3); + fargs[0] = f; + fargs[1] = (jl_value_t*)m; + fargs[2] = syntax_version; + jl_apply(fargs, 3); + JL_GC_POP(); + } + ct->world_age = last_age; +} + +JL_DLLEXPORT jl_module_t *jl_begin_new_module(jl_module_t *parent_module, jl_sym_t *name, jl_value_t *syntax_version, int std_imports, const char *filename, int lineno) { jl_task_t *ct = jl_current_task; @@ -133,17 +151,7 @@ JL_DLLEXPORT jl_module_t *jl_begin_new_module(jl_module_t *parent_module, jl_sym // add standard imports unless baremodule if (std_imports && jl_base_module != NULL) { - jl_module_t *base = jl_add_standard_imports(newm); - jl_datatype_t *include_into = (jl_datatype_t *)jl_get_global(base, jl_symbol("IncludeInto")); - if (include_into) { - form = jl_new_struct(include_into, newm); - jl_set_initial_const(newm, jl_symbol("include"), form, 0); - } - jl_datatype_t *eval_into = (jl_datatype_t *)jl_get_global(jl_core_module, jl_symbol("EvalInto")); - if (eval_into) { - form = jl_new_struct(eval_into, newm); - jl_set_initial_const(newm, jl_symbol("eval"), form, 0); - } + jl_setup_new_module(newm, syntax_version); } if (parent_module == jl_main_module && name == jl_symbol("Base") && jl_base_module == NULL) { @@ -216,21 +224,28 @@ JL_DLLEXPORT void jl_end_new_module(jl_module_t *newm) { static jl_value_t *jl_eval_module_expr(jl_module_t *parent_module, jl_expr_t *ex, const char **toplevel_filename, int *toplevel_lineno) { assert(ex->head == jl_module_sym); - if (jl_array_nrows(ex->args) != 3 || !jl_is_expr(jl_exprarg(ex, 2))) { - jl_error("syntax: malformed module expression"); + + jl_value_t *syntax_version = jl_nothing; + int idx = 0; + if (!jl_is_bool(jl_exprarg(ex, idx))) { + syntax_version = jl_exprarg(ex, idx++); } - if (((jl_expr_t *)(jl_exprarg(ex, 2)))->head != jl_symbol("block")) { - jl_error("syntax: module expression third argument must be a block"); + if (jl_array_nrows(ex->args) != idx+3 || !jl_is_expr(jl_exprarg(ex, idx+2))) { + jl_error("syntax: malformed module expression"); } - jl_array_t *stmts = ((jl_expr_t*)jl_exprarg(ex, 2))->args; - int std_imports = (jl_exprarg(ex, 0) == jl_true); - jl_sym_t *name = (jl_sym_t*)jl_exprarg(ex, 1); + int std_imports = (jl_exprarg(ex, idx++) == jl_true); + jl_sym_t *name = (jl_sym_t*)jl_exprarg(ex, idx++); if (!jl_is_symbol(name)) { jl_type_error("module", (jl_value_t*)jl_symbol_type, (jl_value_t*)name); } + if (((jl_expr_t *)(jl_exprarg(ex, idx)))->head != jl_symbol("block")) { + jl_error("syntax: module expression third argument must be a block"); + } + jl_array_t *stmts = ((jl_expr_t*)jl_exprarg(ex, idx))->args; + int lineno = 0; const char *filename = "none"; if (jl_array_nrows(stmts) > 0) { @@ -243,7 +258,7 @@ static jl_value_t *jl_eval_module_expr(jl_module_t *parent_module, jl_expr_t *ex } } - jl_module_t *newm = jl_begin_new_module(parent_module, name, std_imports, filename, lineno); + jl_module_t *newm = jl_begin_new_module(parent_module, name, syntax_version, std_imports, filename, lineno); JL_GC_PROMISE_ROOTED(newm); // Rooted in jl_current_modules jl_eval_toplevel_stmts(newm, stmts, 1, 0, toplevel_filename, toplevel_lineno); jl_end_new_module(newm); @@ -830,7 +845,7 @@ static jl_value_t *jl_parse_eval_all(jl_module_t *module, jl_value_t *text, JL_GC_PUSH3(&ast, &result, &expression); ast = jl_svecref(jl_parse(jl_string_data(text), jl_string_len(text), - filename, 1, 0, (jl_value_t*)jl_all_sym), 0); + filename, 1, 0, (jl_value_t*)jl_all_sym, module), 0); if (!jl_is_expr(ast) || ((jl_expr_t*)ast)->head != jl_toplevel_sym) { jl_errorf("jl_parse_all() must generate a top level expression"); } diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index de246f7971c30..0c31315e9bea1 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -740,7 +740,7 @@ function run_frontend(repl::BasicREPL, backend::REPLBackendRef) rethrow() end end - ast = Base.parse_input_line(line) + ast = Base.parse_input_line(line; mod=Base.active_module(repl)) (isa(ast,Expr) && ast.head === :incomplete) || break end if !isempty(line) @@ -814,7 +814,8 @@ REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers()) mutable struct ShellCompletionProvider <: CompletionProvider end struct LatexCompletions <: CompletionProvider end -Base.active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module +Base.active_module(mistate::MIState) = mistate.active_module +Base.active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : Base.active_module(mistate) Base.active_module(::AbstractREPL) = Main Base.active_module(d::REPLDisplay) = Base.active_module(d.repl) @@ -1117,7 +1118,7 @@ end LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist) function return_callback(s) - ast = Base.parse_input_line(takestring!(copy(LineEdit.buffer(s))), depwarn=false) + ast = Base.parse_input_line(takestring!(copy(LineEdit.buffer(s))); mod=Base.active_module(s), depwarn=false) return !(isa(ast, Expr) && ast.head === :incomplete) end @@ -1290,7 +1291,7 @@ function setup_interface( repl = repl, complete = replc, # When we're done transform the entered line into a call to helpmode function - on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module), + on_done = respond(line::String->helpmode(outstream(repl), line, Base.active_module(repl)), repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false)) @@ -1371,7 +1372,7 @@ function setup_interface( help_mode.hist = hp dummy_pkg_mode.hist = hp - julia_prompt.on_done = respond(x->Base.parse_input_line(x,filename=repl_filename(repl,hp)), repl, julia_prompt) + julia_prompt.on_done = respond(x->Base.parse_input_line(x; filename=repl_filename(repl,hp), mod=Base.active_module(repl)), repl, julia_prompt) shell_prompt_len = length(SHELL_PROMPT) help_prompt_len = length(HELP_PROMPT) @@ -1535,7 +1536,7 @@ function setup_interface( dump_tail = false nl_pos = findfirst('\n', input[oldpos:end]) if s.current_mode == julia_prompt - ast, pos = Meta.parse(input, oldpos, raise=false, depwarn=false) + ast, pos = Meta.parse(input, oldpos, raise=false, depwarn=false, mod=Base.active_module(s)) if (isa(ast, Expr) && (ast.head === :error || ast.head === :incomplete)) || (pos > ncodeunits(input) && !endswith(input, '\n')) # remaining text is incomplete (an error, or parser ran to the end but didn't stop with a newline): @@ -1791,7 +1792,7 @@ function run_frontend(repl::StreamREPL, backend::REPLBackendRef) end line = readline(repl.stream, keep=true) if !isempty(line) - ast = Base.parse_input_line(line) + ast = Base.parse_input_line(line; mod=Base.active_module(repl)) if have_color print(repl.stream, Base.color_normal) end diff --git a/stdlib/REPL/src/REPLCompletions.jl b/stdlib/REPL/src/REPLCompletions.jl index 10dfd70eda2db..b2b510e8e1e10 100644 --- a/stdlib/REPL/src/REPLCompletions.jl +++ b/stdlib/REPL/src/REPLCompletions.jl @@ -993,6 +993,7 @@ end function completions(string::String, pos::Int, context_module::Module=Main, shift::Bool=true, hint::Bool=false) # filename needs to be string so macro can be evaluated + # TODO: JuliaSyntax version API here node = parseall(CursorNode, string, ignore_errors=true, keep_parens=true, filename="none") cur = @something seek_pos(node, pos) node diff --git a/test/loading.jl b/test/loading.jl index 5bcd18ff26843..e125fb7e3e9fe 100644 --- a/test/loading.jl +++ b/test/loading.jl @@ -1830,8 +1830,8 @@ end Base64_key = Base.PkgId(Base.UUID("2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"), "Base64") oldBase64 = Base.unreference_module(Base64_key) cc = Base.compilecache(Base64_key) - sourcepath = Base.locate_package(Base64_key) - @test Base.stale_cachefile(Base64_key, UInt128(0), sourcepath, cc[1]) !== true + sourcespec = Base.locate_package_load_spec(Base64_key) + @test Base.stale_cachefile(Base64_key, UInt128(0), sourcespec, cc[1]) !== true empty!(DEPOT_PATH) Base.require_stdlib(Base64_key) push!(DEPOT_PATH, depot_path) @@ -1868,3 +1868,40 @@ end module M58272_to end @eval M58272_to import ..M58272_1: M58272_2.y, x @test @eval M58272_to x === 1 + +@testset "Syntax Versioning" begin + old_load_path = copy(LOAD_PATH) + try + # Test implicit environments (packages loaded from directories) + push!(LOAD_PATH, joinpath(@__DIR__, "project", "SyntaxVersioning", "implicit")) + # Explicit syntax.julia_version = "1.13" + @test invokelatest(getglobal, (@eval (using Versioned1; Versioned1)), :ver) == v"1.13" + # Explicit syntax.julia_version = "1.14" + @test invokelatest(getglobal, (@eval (using Versioned2; Versioned2)), :ver) == v"1.14" + # Inherited from compat.julia = "1.13-2" + @test invokelatest(getglobal, (@eval (using Versioned3; Versioned3)), :ver) == v"1.13" + # No syntax.julia_version, falls back to current VERSION + @test invokelatest(getglobal, (@eval (using Versioned4; Versioned4)), :ver) == VersionNumber(VERSION.major, VERSION.minor) + # Inherited from compat.julia = "1.14-2" + @test invokelatest(getglobal, (@eval (using Versioned5; Versioned5)), :ver) == v"1.14" + finally + copy!(LOAD_PATH, old_load_path) + end + + # Test explicit environments (packages loaded from Manifest.toml) + old_load_path = copy(LOAD_PATH) + old_active_project = Base.ACTIVE_PROJECT[] + try + explicit_env = joinpath(@__DIR__, "project", "SyntaxVersioning", "explicit") + Base.ACTIVE_PROJECT[] = joinpath(explicit_env, "Project.toml") + empty!(LOAD_PATH) + push!(LOAD_PATH, "@") + # syntax.julia_version from Manifest = "1.13" + @test invokelatest(getglobal, (@eval (using VersionedDep1; VersionedDep1)), :ver) == v"1.13" + # syntax.julia_version from Manifest = "1.14" + @test invokelatest(getglobal, (@eval (using VersionedDep2; VersionedDep2)), :ver) == v"1.14" + finally + Base.ACTIVE_PROJECT[] = old_active_project + copy!(LOAD_PATH, old_load_path) + end +end diff --git a/test/project/SyntaxVersioning/explicit/Manifest.toml b/test/project/SyntaxVersioning/explicit/Manifest.toml new file mode 100644 index 0000000000000..23fdf14c0ddc9 --- /dev/null +++ b/test/project/SyntaxVersioning/explicit/Manifest.toml @@ -0,0 +1,14 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.14.0-DEV" +manifest_format = "2.1" + +[[deps.VersionedDep1]] +path = "VersionedDep1" +uuid = "f08855a0-36cb-4a32-8ae5-a227b709c612" +syntax.julia_version = "1.13.0" + +[[deps.VersionedDep2]] +path = "VersionedDep2" +uuid = "e127e659-a899-4a00-b565-5b74face18ba" +syntax.julia_version = "1.14.0" diff --git a/test/project/SyntaxVersioning/explicit/Project.toml b/test/project/SyntaxVersioning/explicit/Project.toml new file mode 100644 index 0000000000000..d5cfbb156a3a7 --- /dev/null +++ b/test/project/SyntaxVersioning/explicit/Project.toml @@ -0,0 +1,3 @@ +[deps] +VersionedDep1 = "f08855a0-36cb-4a32-8ae5-a227b709c612" +VersionedDep2 = "e127e659-a899-4a00-b565-5b74face18ba" diff --git a/test/project/SyntaxVersioning/explicit/VersionedDep1/Project.toml b/test/project/SyntaxVersioning/explicit/VersionedDep1/Project.toml new file mode 100644 index 0000000000000..0b35c64ade6c9 --- /dev/null +++ b/test/project/SyntaxVersioning/explicit/VersionedDep1/Project.toml @@ -0,0 +1,3 @@ +name = "VersionedDep1" +uuid = "f08855a0-36cb-4a32-8ae5-a227b709c612" +syntax.julia_version = "1.13" diff --git a/test/project/SyntaxVersioning/explicit/VersionedDep1/src/VersionedDep1.jl b/test/project/SyntaxVersioning/explicit/VersionedDep1/src/VersionedDep1.jl new file mode 100644 index 0000000000000..6154c45f97ee2 --- /dev/null +++ b/test/project/SyntaxVersioning/explicit/VersionedDep1/src/VersionedDep1.jl @@ -0,0 +1,3 @@ +module VersionedDep1 + const ver = (@Base.Experimental.VERSION).syntax +end diff --git a/test/project/SyntaxVersioning/explicit/VersionedDep2/Project.toml b/test/project/SyntaxVersioning/explicit/VersionedDep2/Project.toml new file mode 100644 index 0000000000000..2876fa5f51e55 --- /dev/null +++ b/test/project/SyntaxVersioning/explicit/VersionedDep2/Project.toml @@ -0,0 +1,3 @@ +name = "VersionedDep2" +uuid = "e127e659-a899-4a00-b565-5b74face18ba" +syntax.julia_version = "1.14" diff --git a/test/project/SyntaxVersioning/explicit/VersionedDep2/src/VersionedDep2.jl b/test/project/SyntaxVersioning/explicit/VersionedDep2/src/VersionedDep2.jl new file mode 100644 index 0000000000000..f3bf4197e66c1 --- /dev/null +++ b/test/project/SyntaxVersioning/explicit/VersionedDep2/src/VersionedDep2.jl @@ -0,0 +1,3 @@ +module VersionedDep2 + const ver = (@Base.Experimental.VERSION).syntax +end diff --git a/test/project/SyntaxVersioning/implicit/Versioned1/Project.toml b/test/project/SyntaxVersioning/implicit/Versioned1/Project.toml new file mode 100644 index 0000000000000..30d8e2686b73f --- /dev/null +++ b/test/project/SyntaxVersioning/implicit/Versioned1/Project.toml @@ -0,0 +1,3 @@ +name = "Versioned1" +uuid = "5039f352-f8db-42c3-a2c5-1d61ed1e55b8" +syntax.julia_version = "1.13" diff --git a/test/project/SyntaxVersioning/implicit/Versioned1/src/Versioned1.jl b/test/project/SyntaxVersioning/implicit/Versioned1/src/Versioned1.jl new file mode 100644 index 0000000000000..0622ad832c31e --- /dev/null +++ b/test/project/SyntaxVersioning/implicit/Versioned1/src/Versioned1.jl @@ -0,0 +1,3 @@ +module Versioned1 + const ver = (@Base.Experimental.VERSION).syntax +end diff --git a/test/project/SyntaxVersioning/implicit/Versioned2/Project.toml b/test/project/SyntaxVersioning/implicit/Versioned2/Project.toml new file mode 100644 index 0000000000000..576b6a53d083f --- /dev/null +++ b/test/project/SyntaxVersioning/implicit/Versioned2/Project.toml @@ -0,0 +1,3 @@ +name = "Versioned2" +uuid = "3a4c0187-8e98-47c1-abc0-783d1a175621" +syntax.julia_version = "1.14" diff --git a/test/project/SyntaxVersioning/implicit/Versioned2/src/Versioned2.jl b/test/project/SyntaxVersioning/implicit/Versioned2/src/Versioned2.jl new file mode 100644 index 0000000000000..0cf90ce3dabde --- /dev/null +++ b/test/project/SyntaxVersioning/implicit/Versioned2/src/Versioned2.jl @@ -0,0 +1,3 @@ +module Versioned2 + const ver = (@Base.Experimental.VERSION).syntax +end diff --git a/test/project/SyntaxVersioning/implicit/Versioned3/Project.toml b/test/project/SyntaxVersioning/implicit/Versioned3/Project.toml new file mode 100644 index 0000000000000..3522ebf04a699 --- /dev/null +++ b/test/project/SyntaxVersioning/implicit/Versioned3/Project.toml @@ -0,0 +1,5 @@ +name = "Versioned3" +uuid = "06d511b3-69b4-4d20-8d3b-d39263331254" + +[compat] +julia = "1.13 - 2" diff --git a/test/project/SyntaxVersioning/implicit/Versioned3/src/Versioned3.jl b/test/project/SyntaxVersioning/implicit/Versioned3/src/Versioned3.jl new file mode 100644 index 0000000000000..72ba851940b88 --- /dev/null +++ b/test/project/SyntaxVersioning/implicit/Versioned3/src/Versioned3.jl @@ -0,0 +1,3 @@ +module Versioned3 + const ver = (@Base.Experimental.VERSION).syntax +end diff --git a/test/project/SyntaxVersioning/implicit/Versioned4/Project.toml b/test/project/SyntaxVersioning/implicit/Versioned4/Project.toml new file mode 100644 index 0000000000000..0bfee425a3cb7 --- /dev/null +++ b/test/project/SyntaxVersioning/implicit/Versioned4/Project.toml @@ -0,0 +1,2 @@ +name = "Versioned4" +uuid = "3a4c0187-8e98-47c1-abc0-783d1a175621" diff --git a/test/project/SyntaxVersioning/implicit/Versioned4/src/Versioned4.jl b/test/project/SyntaxVersioning/implicit/Versioned4/src/Versioned4.jl new file mode 100644 index 0000000000000..837e942386fc3 --- /dev/null +++ b/test/project/SyntaxVersioning/implicit/Versioned4/src/Versioned4.jl @@ -0,0 +1,3 @@ +module Versioned4 + const ver = (@Base.Experimental.VERSION).syntax +end diff --git a/test/project/SyntaxVersioning/implicit/Versioned5/Project.toml b/test/project/SyntaxVersioning/implicit/Versioned5/Project.toml new file mode 100644 index 0000000000000..f17617f42ce72 --- /dev/null +++ b/test/project/SyntaxVersioning/implicit/Versioned5/Project.toml @@ -0,0 +1,5 @@ +name = "Versioned5" +uuid = "1805a4d1-8cc9-402d-b9fb-3f94ad9a89b5" + +[compat] +julia = "1.14 - 2" diff --git a/test/project/SyntaxVersioning/implicit/Versioned5/src/Versioned5.jl b/test/project/SyntaxVersioning/implicit/Versioned5/src/Versioned5.jl new file mode 100644 index 0000000000000..43a6dbd102291 --- /dev/null +++ b/test/project/SyntaxVersioning/implicit/Versioned5/src/Versioned5.jl @@ -0,0 +1,3 @@ +module Versioned5 + const ver = (@Base.Experimental.VERSION).syntax +end