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..a5c5284f5d263 100644 --- a/base/Base_compiler.jl +++ b/base/Base_compiler.jl @@ -2,8 +2,8 @@ module Base -Core._import(Base, Core, :_eval_import, :_eval_import, true) -Core._import(Base, Core, :_eval_using, :_eval_using, true) +Core._import(Base, Core, :_eval_import, :_eval_import, 0x1) +Core._import(Base, Core, :_eval_using, :_eval_using, 0x1) using .Core.Intrinsics, .Core.IR @@ -141,6 +141,26 @@ 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)) + parent = ccall(:jl_module_parent, Ref{Module}, (Any,), mod) + if parent === mod + else + Core._import(mod, parent, :_internal_module_strict_flags, + :_internal_module_strict_flags, 0x3) + end + 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..d9ece6e67d20c 100644 --- a/base/boot.jl +++ b/base/boot.jl @@ -726,7 +726,7 @@ let a = getfield(paths, i).args length(a) == 1 || fail() s = getindex(a, 1) - Core._import(to, from, s, s, explicit) + Core._import(to, from, s, s, explicit ? 0x1 : 0x0) i += 1 end end @@ -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/docs/basedocs.jl b/base/docs/basedocs.jl index c4dcc90baf945..7dc82b19396a6 100644 --- a/base/docs/basedocs.jl +++ b/base/docs/basedocs.jl @@ -2782,7 +2782,7 @@ See also [`global`](@ref), [`setglobal!`](@ref), [`get_binding_type`](@ref Core. Core.declare_global """ - declare_const(module::Module, name::Symbol, [x]) + declare_const(module::Module, name::Symbol, [x, [flags::UInt8]]) Create or replace the constant `name` in `module` with the new value `x`. When replacing, `x` does not need to have the same type as the original constant. @@ -2817,11 +2817,14 @@ See also [`const`](@ref). Core.declare_const """ - _import(to::Module, from::Module, asname::Symbol, [sym::Symbol, imported::Bool]) + _import(to::Module, from::Module, asname::Symbol, [sym::Symbol, flags::Uint8]) With all five arguments, imports `sym` from module `from` into `to` with name -`asname`. `imported` is true for bindings created with `import` (set it to -false for `using A: ...`). +`asname`. `flags` is a bitmask of JL_IMPORT_FLAG_* flags. + - JL_IMPORT_FLAG_EXPLICIT is set for bindings created with `import` (unset for `using A: ...`). + - JL_IMPORT_FLAG_ALLOW_UNDEF allows the imported binding to be undefined at + import time (this is ordinarily a warning). Note that the binding may be + resolved later if it becomes available in the imported module. With only the first three arguments, creates a binding for the module `from` with name `asname` in `to`. diff --git a/base/experimental.jl b/base/experimental.jl index 730a42eb1717d..aaf99c184da06 100644 --- a/base/experimental.jl +++ b/base/experimental.jl @@ -746,4 +746,206 @@ 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 + +# N.B.: Must match the values in julia_internal.h +const JL_STRICT_IMPORT_TYPE = 0x01 +const JL_STRICT_LITERAL_ITERATORS = 0x02 + +function strict_mode_flags(flag::Symbol) + if flag === :typeimports + return JL_STRICT_IMPORT_TYPE + elseif flag === :nointliteraliterators + return JL_STRICT_LITERAL_ITERATORS + else + error("unknown strict mode flag: $flag") + end +end +strict_mode_flags(flags) = + mapreduce(strict_mode_flags, |, flags; init=0x0) + +function set_strict_mode(mod::Module, @nospecialize(flags)) + Core.declare_const(mod, :_internal_module_strict_flags, + strict_mode_flags(flags), Base.JL_CONST_MAY_REPLACE_IMPORTS) + return nothing +end + +""" + Base.Experimental.@strict flags + +Set the `strict` mode for the current module to all flags specified in `flags`. +Each `strict` mode flag is an independent option that disallows certain syntax or +semantics that may be undesirable in *some* contexts. Package authors should +consider which flags are appropriate for their package and set them at the top +of the applicable module. + +# General semantics and philosophy + +Note that additional strict mode flags are not necessarily *safer* or *better* +in any way; they are a reflection of of the reality that different users have +different tradeoffs for their codebases and what may be a sensible restriction +in a mature package could be very annoying in the REPL. + +As designed, there are several general guidelines that apply to strict mode flags. +To the extent possible, they should be kept for future flag additions. + +1. Code that evaluates without error in strict mode should also evaluate without + error under the ordinary julia execution semantics. + +2. Strict mode should not affect parsing. If it is desirable to disallow a + particular syntax pattern, it should be recognized at the lowering stage. + If this is currently not possible, the parser should be modified to emit an + appropriate marker that can be checked at lowering time. + +3. Strict mode is not intended for for issues that are clearly bugs. + Those should instead use the syntax versioning mechanism (see + [`Base.Experimental.@set_syntax_version`](@ref)). However, `strict` mode flags + that gain widespread adoption may eventually be considered as candidates for + syntax evolution. + +Strict mode flags are automatically inherited by submodules, but can be +overriden by an explicit `@strict` invocation in the submodule. Strict mode +flags are partitioned by world age. + +# Specifying flags + +The `flags` expression is runtime-evaluated and should evaluate to a collection +of `Symbols` as specified below. In addition, nested collections of symbols are +allowed and will be flattened. This is intended to support specifying strict +mode flags in a central location and enforcing them across multiple dependents. + +# Currently supported flags + + * `typeimports` + + This flag turns the 1.12 warning for implicit import of types into an error. + Note that the implicit import default may be removed in a future Julia syntax + iteration, in which case this flag will become a no-op for such versions. + + ```jldoctest + julia> @Base.Experimental.strict :typeimports + + julia> String(x) = 1 + ERROR: `@strict :typeimports` disallows extending types without explicit import in TypeImports: function Base.String must be explicitly imported to be extended + ``` + + * `:nointliteraliterators` + + Disallows (at the lowering stages) literal integers as iterators in `for` loops. + This protects against expressions like `for i in 10` which are commonly intended + to be `for i in 1:10`. + + ```jldoctest + julia> for i in 10 + println(i) + end + 10 + + julia> @Base.Experimental.strict :nointliteraliterators + + julia> for i in 10 + println(i) + end + ERROR: syntax: `@strict :nointliteraliterators` disallows integer literal iterators here around none:1 + Stacktrace: + [1] top-level scope + @ none:1 +""" +macro strict(flags) + Expr(:call, set_strict_mode, __module__, esc(flags)) +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/module.jl b/base/module.jl index 9cf74f81fccae..3ffb55ed2b650 100644 --- a/base/module.jl +++ b/base/module.jl @@ -113,7 +113,7 @@ function _eval_import(imported::Bool, to::Module, from::Union{Expr, Nothing}, pa if name !== nothing asname = asname === nothing ? name : asname check_macro_rename(name, asname, keyword) - Core._import(to, m, asname, name, imported) + Core._import(to, m, asname, name, imported ? JL_IMPORT_FLAG_EXPLICIT : 0x0) else Core._import(to, m, asname === nothing ? nameof(m) : asname) 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..c0b6448af47cc 100644 --- a/base/runtime_internals.jl +++ b/base/runtime_internals.jl @@ -262,6 +262,11 @@ const BINDING_FLAG_ANY_IMPLICIT_EDGES = 0x8 const JL_MODULE_USING_REEXPORT = 0x1 +const JL_IMPORT_FLAG_EXPLICIT = 0x1 +const JL_IMPORT_FLAG_ALLOW_UNDEF = 0x2 + +const JL_CONST_MAY_REPLACE_IMPORTS = 0x1 + is_defined_const_binding(kind::UInt8) = (kind == PARTITION_KIND_CONST || kind == PARTITION_KIND_CONST_IMPORT || kind == PARTITION_KIND_IMPLICIT_CONST || kind == PARTITION_KIND_BACKDATED_CONST) is_some_const_binding(kind::UInt8) = (is_defined_const_binding(kind) || kind == PARTITION_KIND_UNDEF_CONST) is_some_imported(kind::UInt8) = (kind == PARTITION_KIND_IMPLICIT_GLOBAL || kind == PARTITION_KIND_IMPLICIT_CONST || kind == PARTITION_KIND_EXPLICIT || kind == PARTITION_KIND_IMPORTED) @@ -1800,9 +1805,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..8eba3040ef235 100644 --- a/src/ast.c +++ b/src/ast.c @@ -39,6 +39,7 @@ typedef struct _jl_ast_context_t { value_t ssavalue_sym; value_t slot_sym; jl_module_t *module; // context module for `current-julia-module-counter` + uint8_t module_strict_flags; // cached strict mode flags for module struct _jl_ast_context_t *next; // invasive list pointer for getting free contexts } jl_ast_context_t; @@ -151,11 +152,19 @@ static value_t fl_julia_scalar(fl_context_t *fl_ctx, value_t *args, uint32_t nar return fl_ctx->F; } +static value_t fl_module_strict_flags(fl_context_t *fl_ctx, value_t *args, uint32_t nargs) +{ + argcount(fl_ctx, "julia-current-module-flags", nargs, 0); + jl_ast_context_t *ctx = jl_ast_ctx(fl_ctx); + return fixnum(ctx->module_strict_flags); +} + static jl_value_t *scm_to_julia_(fl_context_t *fl_ctx, value_t e, jl_module_t *mod); static const builtinspec_t julia_flisp_ast_ext[] = { { "defined-julia-global", fl_defined_julia_global }, // TODO: can we kill this safepoint { "current-julia-module-counter", fl_module_unique_name }, + { "julia-current-module-flags", fl_module_strict_flags }, { "julia-scalar?", fl_julia_scalar }, { NULL, NULL } }; @@ -181,6 +190,7 @@ static void jl_init_ast_ctx(jl_ast_context_t *ctx) JL_NOTSAFEPOINT ctx->ssavalue_sym = symbol(fl_ctx, "ssavalue"); ctx->slot_sym = symbol(fl_ctx, "slot"); ctx->module = NULL; + ctx->module_strict_flags = 0; set(symbol(fl_ctx, "*scopewarn-opt*"), fixnum(jl_options.warn_scope)); } @@ -204,6 +214,8 @@ static jl_ast_context_t *jl_ast_ctx_enter(jl_module_t *m) JL_GLOBALLY_ROOTED JL_ jl_init_ast_ctx(ctx); } ctx->module = m; + if (m) + ctx->module_strict_flags = jl_module_strict_flags(m, jl_current_task->world_age); return ctx; } @@ -211,6 +223,7 @@ static void jl_ast_ctx_leave(jl_ast_context_t *ctx) { uv_mutex_lock(&flisp_lock); ctx->module = NULL; + ctx->module_strict_flags = 0; ctx->next = jl_ast_ctx_freed; jl_ast_ctx_freed = ctx; uv_mutex_unlock(&flisp_lock); @@ -1016,8 +1029,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 +1055,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 +1258,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 +1325,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 +1370,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 +1383,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/ast.scm b/src/ast.scm index ea538e0aede4e..a29da05b036e2 100644 --- a/src/ast.scm +++ b/src/ast.scm @@ -489,12 +489,13 @@ (define vinfo:type cadr) (define (vinfo:set-type! v t) (set-car! (cdr v) t)) -(define (vinfo:capt v) (< 0 (logand (caddr v) 1))) -(define (vinfo:asgn v) (< 0 (logand (caddr v) 2))) -(define (vinfo:never-undef v) (< 0 (logand (caddr v) 4))) -(define (vinfo:read v) (< 0 (logand (caddr v) 8))) -(define (vinfo:sa v) (< 0 (logand (caddr v) 16))) -(define (vinfo:nospecialize v) (< 0 (logand (caddr v) 128))) +(define (test-bit x b) (< 0 (logand x b))) +(define (vinfo:capt v) (test-bit (caddr v) 1)) +(define (vinfo:asgn v) (test-bit (caddr v) 2)) +(define (vinfo:never-undef v) (test-bit (caddr v) 4)) +(define (vinfo:read v) (test-bit (caddr v) 8)) +(define (vinfo:sa v) (test-bit (caddr v) 16)) +(define (vinfo:nospecialize v) (test-bit (caddr v) 128)) (define (set-bit x b val) (if val (logior x b) (logand x (lognot b)))) ;; record whether var is captured (define (vinfo:set-capt! v c) (set-car! (cddr v) (set-bit (caddr v) 1 c))) diff --git a/src/builtins.c b/src/builtins.c index b50e6c247ce46..0269c683d5259 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -1537,13 +1537,18 @@ JL_CALLABLE(jl_f_declare_global) JL_CALLABLE(jl_f_declare_const) { - JL_NARGS(declare_const, 2, 3); + JL_NARGS(declare_const, 2, 4); JL_TYPECHK(declare_const, module, args[0]); if (nargs == 3) JL_TYPECHK(declare_const, symbol, args[1]); jl_binding_t *b = jl_get_module_binding((jl_module_t *)args[0], (jl_sym_t *)args[1], 1); - jl_value_t *val = nargs == 3 ? args[2] : NULL; - jl_declare_constant_val(b, (jl_module_t *)args[0], (jl_sym_t *)args[1], val); + jl_value_t *val = nargs >= 3 ? args[2] : NULL; + uint8_t flags = 0; + if (nargs == 4) { + JL_TYPECHK(declare_const, uint8, args[3]); + flags = jl_unbox_uint8(args[3]); + } + jl_declare_constant_val(b, (jl_module_t *)args[0], (jl_sym_t *)args[1], val, flags); return nargs > 2 ? args[2] : jl_nothing; } @@ -1569,14 +1574,14 @@ JL_CALLABLE(jl_f__import) } else if (nargs == 5) { JL_TYPECHK(_import, symbol, args[3]); - JL_TYPECHK(_import, bool, args[4]); + JL_TYPECHK(_import, uint8, args[4]); jl_module_import(jl_current_task, (jl_module_t *)args[0], (jl_module_t *)args[1], - (jl_sym_t *)args[2], (jl_sym_t *)args[3], args[4] == jl_true); + (jl_sym_t *)args[2], (jl_sym_t *)args[3], jl_unbox_uint8(args[4])); } return jl_nothing; } -// _using(to::Module, from::Module) +// _using(to::Module, from::Module, [flags::UInt8=0]) JL_CALLABLE(jl_f__using) { JL_NARGS(_using, 2, 3); diff --git a/src/gf.c b/src/gf.c index 1d3a9636ddfa9..bc55155a97071 100644 --- a/src/gf.c +++ b/src/gf.c @@ -4454,7 +4454,7 @@ jl_value_t *jl_new_generic_function_with_supertype(jl_sym_t *name, jl_module_t * JL_GC_PUSH1(&ftype); ftype->name->singletonname = name; jl_gc_wb(ftype->name, name); - jl_declare_constant_val3(NULL, module, tname, (jl_value_t*)ftype, PARTITION_KIND_CONST, new_world); + jl_declare_constant_val3(NULL, module, tname, (jl_value_t*)ftype, PARTITION_KIND_CONST, new_world, 0); jl_value_t *f = jl_new_struct(ftype); ftype->instance = f; jl_gc_wb(ftype, f); diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index f3a57c6699c08..adb9d4bce9469 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -1953,6 +1953,12 @@ (if ,g ,g ,(loop (cdr tail))))))))))) +(define (check-disallowed-iterator itr) + ;; N.B.: Must match JL_STRICT_LITERAL_ITERATORS in julia_internal.h + (if (and (test-bit (julia-current-module-flags) 2) (integer? itr)) + (error "`@strict :nointliteraliterators` disallows integer literal iterators here") + itr)) + (define (expand-for lhss itrs body) (define (outer? x) (and (pair? x) (eq? (car x) 'outer))) (let ((copied-vars ;; variables not declared `outer` are copied in the innermost loop @@ -1987,7 +1993,7 @@ (soft-let (block ,@(map (lambda (v) `(= ,v ,v)) copied-vars)) ,body)) `(scope-block ,body)))) - `(block (= ,coll ,(car itrs)) + `(block (= ,coll ,(check-disallowed-iterator (car itrs))) (local ,next) (= ,next (call (top iterate) ,coll)) ;; TODO avoid `local declared twice` error from this diff --git a/src/julia.h b/src/julia.h index 2937ec44eb2f5..38e23f003a00b 100644 --- a/src/julia.h +++ b/src/julia.h @@ -769,6 +769,7 @@ static const uint8_t PARTITION_FLAG_DEPWARN = 0x40; // this flag is set during implicit resolution and can be removed if the resolution changes. static const uint8_t PARTITION_FLAG_IMPLICITLY_EXPORTED = 0x80; + #if defined(_COMPILER_MICROSOFT_) #define JL_ALIGNED_ATTR(alignment) \ __declspec(align(alignment)) @@ -872,6 +873,13 @@ struct _jl_module_using { // Flags for _jl_module_using.flags static const uint8_t JL_MODULE_USING_REEXPORT = 0x1; +// Flags for jl_module_import +static const uint8_t JL_IMPORT_FLAG_EXPLICIT = 0x1; +static const uint8_t JL_IMPORT_FLAG_ALLOW_UNDEF = 0x2; + +// Flags for constant declaration +static const uint8_t JL_CONST_MAY_REPLACE_IMPORTS = 0x1; + struct _jl_globalref_t { JL_DATA_TYPE jl_module_t *mod; @@ -2043,9 +2051,9 @@ JL_DLLEXPORT jl_value_t *jl_checked_swap(jl_binding_t *b, jl_module_t *mod, jl_s JL_DLLEXPORT jl_value_t *jl_checked_replace(jl_binding_t *b, jl_module_t *mod, jl_sym_t *var, jl_value_t *expected, jl_value_t *rhs); JL_DLLEXPORT jl_value_t *jl_checked_modify(jl_binding_t *b, jl_module_t *mod, jl_sym_t *var, jl_value_t *op, jl_value_t *rhs); JL_DLLEXPORT jl_value_t *jl_checked_assignonce(jl_binding_t *b, jl_module_t *mod, jl_sym_t *var, jl_value_t *rhs JL_MAYBE_UNROOTED); -JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val(jl_binding_t *b JL_ROOTING_ARGUMENT, jl_module_t *mod, jl_sym_t *var, jl_value_t *val JL_ROOTED_ARGUMENT JL_MAYBE_UNROOTED); -JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val2(jl_binding_t *b JL_ROOTING_ARGUMENT, jl_module_t *mod, jl_sym_t *var, jl_value_t *val JL_ROOTED_ARGUMENT JL_MAYBE_UNROOTED, enum jl_partition_kind); -JL_DLLEXPORT void jl_module_import(jl_task_t *ct, jl_module_t *to, jl_module_t *from, jl_sym_t *asname, jl_sym_t *s, int explici); +JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val(jl_binding_t *b JL_ROOTING_ARGUMENT, jl_module_t *mod, jl_sym_t *var, jl_value_t *val JL_ROOTED_ARGUMENT JL_MAYBE_UNROOTED, uint8_t flags); +JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val2(jl_binding_t *b JL_ROOTING_ARGUMENT, jl_module_t *mod, jl_sym_t *var, jl_value_t *val JL_ROOTED_ARGUMENT JL_MAYBE_UNROOTED, enum jl_partition_kind, uint8_t flags); +JL_DLLEXPORT void jl_module_import(jl_task_t *ct, jl_module_t *to, jl_module_t *from, jl_sym_t *asname, jl_sym_t *s, uint8_t flags); JL_DLLEXPORT void jl_import_module(jl_task_t *ct, jl_module_t *m, jl_module_t *import, jl_sym_t *asname); JL_DLLEXPORT void jl_module_using(jl_module_t *to, jl_module_t *from, size_t flags); int jl_module_public_(jl_module_t *from, jl_sym_t *s, int exported, size_t new_world); diff --git a/src/julia_internal.h b/src/julia_internal.h index d67a5dce810a2..bece385eaa0fe 100644 --- a/src/julia_internal.h +++ b/src/julia_internal.h @@ -900,7 +900,7 @@ int jl_type_equality_is_identity(jl_value_t *t1, jl_value_t *t2) JL_NOTSAFEPOINT jl_value_t *jl_check_binding_assign_value(jl_binding_t *b JL_PROPAGATES_ROOT, jl_module_t *mod, jl_sym_t *var, jl_value_t *rhs JL_MAYBE_UNROOTED, const char *msg); void jl_binding_set_type(jl_binding_t *b, jl_module_t *mod, jl_sym_t *sym, jl_value_t *ty); JL_DLLEXPORT void jl_declare_global(jl_module_t *m, jl_value_t *arg, jl_value_t *set_type, int strong); -JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val3(jl_binding_t *b JL_ROOTING_ARGUMENT, jl_module_t *mod, jl_sym_t *var, jl_value_t *val JL_ROOTED_ARGUMENT JL_MAYBE_UNROOTED, enum jl_partition_kind, size_t new_world) JL_GLOBALLY_ROOTED; +JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val3(jl_binding_t *b JL_ROOTING_ARGUMENT, jl_module_t *mod, jl_sym_t *var, jl_value_t *val JL_ROOTED_ARGUMENT JL_MAYBE_UNROOTED, enum jl_partition_kind, size_t new_world, uint8_t flags) JL_GLOBALLY_ROOTED; JL_DLLEXPORT jl_value_t *jl_toplevel_eval_flex(jl_module_t *m, jl_value_t *e, int fast, int expanded, const char **toplevel_filename, int *toplevel_lineno); void jl_module_initial_using(jl_module_t *to, jl_module_t *from); @@ -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); @@ -2122,6 +2123,12 @@ JL_DLLIMPORT int jl_getFunctionInfo(jl_frame_t **frames, uintptr_t pointer, int // n.b. this might be called from unmanaged thread: JL_DLLIMPORT uint64_t jl_getUnwindInfo(uint64_t dwBase); +// Strict mode flags +static const uint8_t JL_STRICT_IMPORT_TYPE = 0x01; +static const uint8_t JL_STRICT_LITERAL_ITERATORS = 0x02; + +JL_DLLEXPORT uint8_t jl_module_strict_flags(jl_module_t *mod, size_t world) JL_NOTSAFEPOINT; + #ifdef __cplusplus } #endif diff --git a/src/method.c b/src/method.c index cee941ae77ddb..1fde104f364c4 100644 --- a/src/method.c +++ b/src/method.c @@ -1221,7 +1221,7 @@ JL_DLLEXPORT jl_value_t *jl_declare_const_gf(jl_module_t *mod, jl_sym_t *name) gf = (jl_value_t*)jl_new_generic_function(name, mod, new_world); // From this point on (if we didn't error), we're committed to raising the world age, // because we've used it to declare the type name. - jl_declare_constant_val3(b, mod, name, gf, PARTITION_KIND_CONST, new_world); + jl_declare_constant_val3(b, mod, name, gf, PARTITION_KIND_CONST, new_world, 0); jl_atomic_store_release(&jl_world_counter, new_world); JL_GC_PROMISE_ROOTED(gf); JL_UNLOCK(&world_counter_lock); diff --git a/src/module.c b/src/module.c index 3cfee6ad0a8c8..c70365ab413d3 100644 --- a/src/module.c +++ b/src/module.c @@ -581,7 +581,7 @@ jl_module_t *jl_new_module_(jl_sym_t *name, jl_module_t *parent, uint8_t default // Precondition: world_counter_lock is held JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val3( jl_binding_t *b, jl_module_t *mod, jl_sym_t *var, jl_value_t *val, - enum jl_partition_kind constant_kind, size_t new_world) + enum jl_partition_kind constant_kind, size_t new_world, uint8_t flags) { jl_binding_partition_t *new_prev_bpart = NULL; JL_GC_PUSH2(&val, &new_prev_bpart); @@ -603,7 +603,8 @@ JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val3( new_bpart = bpart; break; } - } else if (jl_bkind_is_some_explicit_import(kind)) { + } else if (jl_bkind_is_some_explicit_import(kind) && + !(flags & JL_CONST_MAY_REPLACE_IMPORTS)) { jl_errorf("cannot declare %s.%s constant; it was already declared as an import", jl_symbol_name(mod->name), jl_symbol_name(var)); } else if (kind == PARTITION_KIND_GLOBAL) { @@ -1109,6 +1110,11 @@ JL_DLLEXPORT jl_value_t *jl_get_existing_strong_gf(jl_binding_t *b, size_t new_w jl_errorf("invalid method definition in %s: function %s.%s must be explicitly imported to be extended", jl_module_debug_name(b->globalref->mod), from ? jl_module_debug_name(from) : "", jl_symbol_name(b->globalref->name)); } + else if (((jl_module_strict_flags(ownerb->globalref->mod, jl_current_task->world_age) & JL_STRICT_IMPORT_TYPE) != 0)) + { + jl_errorf("`@strict :typeimports` disallows extending types without explicit import in %s: function %s.%s must be explicitly imported to be extended", + jl_module_debug_name(b->globalref->mod), jl_module_debug_name(from), jl_symbol_name(b->globalref->name)); + } else if (!(jl_atomic_fetch_or_relaxed(&b->flags, BINDING_FLAG_DID_PRINT_IMPLICIT_IMPORT_ADMONITION) & BINDING_FLAG_DID_PRINT_IMPLICIT_IMPORT_ADMONITION)) { jl_printf(JL_STDERR, "WARNING: Constructor for type \"%s\" was extended in `%s` without explicit qualification or import.\n" @@ -1247,9 +1253,10 @@ static int eq_bindings(jl_binding_partition_t *owner, jl_binding_t *alias, size_ return owner == alias_bpart; } -// NOTE: we use explici since explicit is a C++ keyword -JL_DLLEXPORT void jl_module_import(jl_task_t *ct, jl_module_t *to, jl_module_t *from, jl_sym_t *asname, jl_sym_t *s, int explici) +JL_DLLEXPORT void jl_module_import(jl_task_t *ct, jl_module_t *to, jl_module_t *from, jl_sym_t *asname, jl_sym_t *s, uint8_t flags) { + // NOTE: we use explici since explicit is a C++ keyword + int explici = (flags & JL_IMPORT_FLAG_EXPLICIT) != 0; check_safe_import_from(from); jl_binding_t *b = jl_get_binding(from, s); jl_binding_partition_t *bpart = jl_get_binding_partition(b, jl_current_task->world_age); @@ -1277,7 +1284,8 @@ JL_DLLEXPORT void jl_module_import(jl_task_t *ct, jl_module_t *to, jl_module_t * jl_binding_partition_t *ownerbpart = bpart; jl_walk_binding_inplace(&ownerb, &ownerbpart, ct->world_age); - if (jl_bkind_is_some_guard(jl_binding_kind(ownerbpart))) { + if (jl_bkind_is_some_guard(jl_binding_kind(ownerbpart)) && + !(flags & JL_IMPORT_FLAG_ALLOW_UNDEF)) { jl_printf(JL_STDERR, "WARNING: Imported binding %s.%s was undeclared at import time during import to %s.\n", jl_symbol_name(from->name), jl_symbol_name(s), @@ -1345,7 +1353,7 @@ JL_DLLEXPORT void jl_import_module(jl_task_t *ct, jl_module_t *JL_NONNULL m, jl_ jl_errorf("importing %s into %s conflicts with an existing global", jl_symbol_name(name), jl_symbol_name(m->name)); } - jl_declare_constant_val2(b, m, name, (jl_value_t*)import, PARTITION_KIND_CONST_IMPORT); + jl_declare_constant_val2(b, m, name, (jl_value_t*)import, PARTITION_KIND_CONST_IMPORT, 0); } void jl_add_usings_backedge(jl_module_t *from, jl_module_t *to) @@ -2210,6 +2218,24 @@ JL_DLLEXPORT void jl_init_restored_module(jl_value_t *mod) } } +JL_DLLEXPORT uint8_t jl_module_strict_flags(jl_module_t *mod, size_t world) JL_NOTSAFEPOINT +{ + jl_binding_t *b = jl_get_module_binding(mod, jl_symbol("_internal_module_strict_flags"), 0); + jl_binding_partition_t *bpart = jl_get_binding_partition(b, world); + // No binding (partition) at all - no strict mode + if (!bpart) + return 0; + jl_walk_binding_inplace(&b, &bpart, jl_current_task->world_age); + if (jl_bkind_is_some_guard(jl_binding_kind(bpart))) + return 0; + // Exists, but not a constant -> error + if (!jl_bkind_is_defined_constant(jl_binding_kind(bpart))) + jl_errorf("malformed _internal_module_strict_flags binding not a constant in module %s", jl_symbol_name(mod->name)); + jl_value_t *val = bpart->restriction; + jl_typeassert(val, (jl_value_t*)jl_uint8_type); + return jl_unbox_uint8(val); +} + #ifdef __cplusplus } #endif 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..2bc7eefcd3a37 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) { @@ -158,7 +166,7 @@ JL_DLLEXPORT jl_module_t *jl_begin_new_module(jl_module_t *parent_module, jl_sym } } else { - jl_declare_constant_val(NULL, parent_module, name, (jl_value_t*)newm); + jl_declare_constant_val(NULL, parent_module, name, (jl_value_t*)newm, 0); } JL_GC_POP(); @@ -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); @@ -551,20 +566,20 @@ static void jl_eval_errorf(jl_module_t *m, const char *filename, int lineno, con JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val2( jl_binding_t *b, jl_module_t *mod, jl_sym_t *var, jl_value_t *val, - enum jl_partition_kind constant_kind) + enum jl_partition_kind constant_kind, uint8_t const_flags) { JL_LOCK(&world_counter_lock); size_t new_world = jl_atomic_load_relaxed(&jl_world_counter) + 1; - jl_binding_partition_t *bpart = jl_declare_constant_val3(b, mod, var, val, constant_kind, new_world); + jl_binding_partition_t *bpart = jl_declare_constant_val3(b, mod, var, val, constant_kind, new_world, const_flags); if (jl_atomic_load_relaxed(&bpart->min_world) == new_world) jl_atomic_store_release(&jl_world_counter, new_world); JL_UNLOCK(&world_counter_lock); return bpart; } -JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val(jl_binding_t *b, jl_module_t *mod, jl_sym_t *var, jl_value_t *val) +JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val(jl_binding_t *b, jl_module_t *mod, jl_sym_t *var, jl_value_t *val, uint8_t const_flags) { - return jl_declare_constant_val2(b, mod, var, val, val ? PARTITION_KIND_CONST : PARTITION_KIND_UNDEF_CONST); + return jl_declare_constant_val2(b, mod, var, val, val ? PARTITION_KIND_CONST : PARTITION_KIND_UNDEF_CONST, const_flags); } static jl_value_t *jl_eval_toplevel_stmts(jl_module_t *JL_NONNULL m, jl_array_t *stmts, int fast, int need_value, const char **toplevel_filename, int *toplevel_lineno) @@ -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/misc.jl b/test/misc.jl index 7df2d5b725b67..2c4655b3e17d1 100644 --- a/test/misc.jl +++ b/test/misc.jl @@ -1675,3 +1675,25 @@ end end end end + +@testset "@strict propagates to child modules" begin + eval(:(module Outer + using Test + @Base.Experimental.strict :nointliteraliterators + module Inner + using Test + @test_throws ErrorException Core.eval(@__MODULE__, :(for i in 10; end)) + end + # But can be overridden in child modules + module Inner2 + @Base.Experimental.strict () + for i in 10; end + end + # ... which does not affect the parent module + @test_throws ErrorException Core.eval(@__MODULE__, :(for i in 10; end)) + # But resetting it in the parent module changes the child module as well + # (e.g. in Revise) + @Base.Experimental.strict () + Core.eval(Inner, :(for i in 10; end)) + 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