From a414ee183a31f255b75f82f0dd110ac64e74ab9c Mon Sep 17 00:00:00 2001 From: Jameson Nash Date: Fri, 18 Sep 2026 09:51:58 -0400 Subject: [PATCH 01/12] codegen: redesign aliasinfo again (#62904) In past AA attempts, we tried to be more accurate, in the belief that would lead to better code. But actually it turns out that LLVM is so terrible at expressing and using aliasing information, that actually using chaotic and obviously worse information leads to much better code outcomes on benchmarks. This has been vaguely known for a long time (e.g. https://discourse.llvm.org/t/separate-aa-metadata-for-load-store-portions-of-memcpy/44617I), so we've been fighting LLVM for a long time, until this gets a proper fix upstream. Claude finds that this particular combination is consistently faster across a range of benchmarks, as well as fixes a variety of soundness risks still remaining in the code. We can redesign this again once LLVM fixes the design. Assisted-by: Claude Code (Opus 5, Fable 5) --- Compiler/test/codegen.jl | 2 +- src/cgutils.cpp | 56 ++-- src/clangsa/GCCheckerReporting.cpp | 4 +- src/codegen.cpp | 399 ++++++++++++++++------------- src/intrinsics.cpp | 11 +- src/llvm-codegen-shared.h | 59 ++++- src/llvm-late-gc-lowering.cpp | 6 +- test/llvmpasses/gcroots.ll | 5 +- test/llvmpasses/refinements.ll | 140 +++++++++- 9 files changed, 453 insertions(+), 229 deletions(-) diff --git a/Compiler/test/codegen.jl b/Compiler/test/codegen.jl index 4af044fe38c7a..0e0d035aa5edb 100644 --- a/Compiler/test/codegen.jl +++ b/Compiler/test/codegen.jl @@ -887,7 +887,7 @@ let io = IOBuffer() code_llvm(io,foo54166, (Vector{Union{Missing,Int}}, Int, Int), dump_module=true, raw=true) str = String(take!(io)) @test !occursin("jtbaa_unionselbyte", str) - @test occursin("jtbaa_arrayselbyte", str) + @test occursin("jtbaa_memoryselbyte", str) end ex54166 = Union{Missing, Int64}[missing -2; missing -2]; diff --git a/src/cgutils.cpp b/src/cgutils.cpp index de05f83a6d4d7..0ee6922e832b2 100644 --- a/src/cgutils.cpp +++ b/src/cgutils.cpp @@ -488,13 +488,12 @@ static SmallVector ExtractTrackedValues(jl_codectx_t &ctx, Value *Src return Ptrs; } -// whether memory in this region cannot be written concurrently, so that -// non-atomic (rather than unordered) loads and stores are permitted -static bool region_is_private(jl_aliasinfo_t::Region r) +// whether this memory cannot be written concurrently, so that non-atomic +// (rather than unordered) loads and stores are permitted +static bool ai_is_private(const jl_aliasinfo_t &ai) { - return r == jl_aliasinfo_t::Region::stack || - r == jl_aliasinfo_t::Region::gcframe || - r == jl_aliasinfo_t::Region::constant; + using Region = jl_aliasinfo_t::Region; + return ai.isConstant() || ai.region == Region::gcframe; } static llvm::SmallVector extract_gc_roots(jl_codectx_t &ctx, Value *data_pointer, jl_datatype_t *typ, size_t npointers, const jl_aliasinfo_t &roots_ai, bool isVolatile=false) @@ -502,7 +501,7 @@ static llvm::SmallVector extract_gc_roots(jl_codectx_t &ctx, Value *da SmallVector gcroots(npointers); if (npointers) { Type *T_prjlvalue = ctx.types().T_prjlvalue; - bool isprivatemem = isa(data_pointer->stripInBoundsOffsets()) || region_is_private(roots_ai.region); + bool isprivatemem = isa(data_pointer->stripInBoundsOffsets()) || ai_is_private(roots_ai); for (size_t i = 0; i < npointers; i++) { Value *field_ptr = emit_ptrgep(ctx, data_pointer, jl_ptr_offset(typ, i) * sizeof(jl_value_t*)); LoadInst *root = ctx.builder.CreateAlignedLoad(T_prjlvalue, field_ptr, Align(sizeof(void*)), isVolatile); @@ -529,7 +528,7 @@ static llvm::SmallVector extract_gc_roots(jl_codectx_t &ctx, const jl_ Type *T_prjlvalue = ctx.types().T_prjlvalue; const jl_aliasinfo_t &roots_ai = val.aliasinfo; Value *p = maybe_decay_tracked(ctx, data_pointer(ctx, val)); - bool isprivatemem = isa(p->stripInBoundsOffsets()) || region_is_private(roots_ai.region); + bool isprivatemem = isa(p->stripInBoundsOffsets()) || ai_is_private(roots_ai); gcroots.resize(npointers, nullptr); for (size_t i = 0; i < npointers; i++) { Value *field_ptr = emit_ptrgep(ctx, p, jl_ptr_offset((jl_datatype_t*)val.typ, i) * sizeof(jl_value_t*)); @@ -1264,13 +1263,10 @@ static void emit_memcpy_llvm(jl_codectx_t &ctx, Value *dst, jl_aliasinfo_t const return; ++EmittedMemcpys; - // the memcpy intrinsic does not allow specifying different alias tags - // for the load part (src_ai) and the store part (dst_ai). - // since the tbaa lattice has to be a tree we unfortunately have - // tbaa(src) ∪ tbaa(dst) = tbaa_root whenever tbaa(src) != tbaa(dst), - // while the merged region metadata keeps the intersection of the - // regions' noalias sets. - auto merged_ai = dst_ai.merge(ctx, src_ai); + // the memcpy intrinsic has no way to give the load part (src_ai) and the store + // part (dst_ai) different alias info, so one claim must cover both; see + // `jl_aliasinfo_t::merge`. + auto merged_ai = dst_ai.merge(src_ai); #if JL_LLVM_VERSION < 210000 ctx.builder.CreateMemCpy(dst, align_dst, src, align_src, sz, is_volatile, merged_ai.tbaa, merged_ai.tbaa_struct, merged_ai.scope, merged_ai.noalias); @@ -1353,7 +1349,7 @@ static void split_value_into(jl_codectx_t &ctx, const jl_cgval_t &x, Align align return; Value *src; std::tie(src, src_ai) = data_pointer_ai(ctx, value_to_pointer(ctx, x)); - bool isprivatemem = isa(src->stripInBoundsOffsets()) || region_is_private(src_ai.region); + bool isprivatemem = isa(src->stripInBoundsOffsets()) || ai_is_private(src_ai); bool hasptr = typ->layout->first_ptr >= 0; size_t npointers = hasptr ? typ->layout->npointers : 0; size_t shrunken_size = split_value_size(typ).first; @@ -1467,9 +1463,9 @@ static std::tuple split_value(jl_codectx_ return "split::" + std::string(jl_symbol_name(typ->name->name)); }); // The alloca contains no pointers (those were split into roots), so this - // write-once copy takes the layout tag of what it holds, as the no-copy + // write-once copy takes the access tag of what it holds, as the no-copy // paths above already do. - jl_aliasinfo_t dst_ai = stack_copy_aliasinfo(ctx, x.aliasinfo, x.typ); + jl_aliasinfo_t dst_ai = private_copy_aliasinfo(ctx, x.aliasinfo, x.typ); split_value_into(ctx, x, x_alignment, alloca, align_dst, dst_ai, false); return std::make_tuple(alloca, std::move(roots), dst_ai); } @@ -1509,7 +1505,7 @@ static void recombine_value(jl_codectx_t &ctx, const jl_cgval_t &x, Value *dst, jl_aliasinfo_t src_ai = x.aliasinfo; size_t npointers = typ->layout->npointers; size_t shrunken_size = split_value_size(typ).first; - bool isprivatemem = isa(dst->stripInBoundsOffsets()) || dst_ai.region == jl_aliasinfo_t::Region::stack; + bool isprivatemem = isa(dst->stripInBoundsOffsets()) || ai_is_private(dst_ai); size_t off = 0; for (size_t i = 0; true; i++) { bool last = i == npointers; @@ -2540,7 +2536,7 @@ static jl_cgval_t typed_load(jl_codectx_t &ctx, Value *ptr, Value *idx_0based, j AllocaInst *lv = emit_static_alloca(ctx, fsz, Align(al)); setName(ctx.emission_context, lv, "immutable_union"); // the copy is a private, write-once stack location - data_ai = stack_copy_aliasinfo(ctx, ai, jltype); + data_ai = private_copy_aliasinfo(ctx, ai, jltype); emit_memcpy(ctx, lv, data_ai, ptr, ai, fsz, Align(al), Align(al)); data = lv; } @@ -3470,7 +3466,7 @@ static bool isLoadFromConstGV(LoadInst *LI) auto load_base = LI->getPointerOperand()->stripInBoundsOffsets(); assert(load_base); // Static analyzer auto gv = dyn_cast(load_base); - if (isLoadFromImmut(LI)) { + if (isLoadFromRootedRegion(LI)) { if (gv) return true; return isLoadFromConstGV(load_base); @@ -3499,9 +3495,16 @@ static jl_aliasinfo_t best_field_aliasinfo(jl_codectx_t &ctx, const jl_cgval_t & if (idx == 1) return ctx.alias().arraysize; } + // A `const` field of an object that is already a constant global: nothing in this + // compilation unit stores to it, so it is described by the immutable `jtbaa_const` + // tag alone. That the tag does not alias the containing struct's is fine for the + // same reason -- there is no store here for a reader to be reordered against. if (strct.V && jl_field_isconst(jt, idx) && isLoadFromConstGV(strct.V)) - return ctx.alias().constant; //TODO: it seems odd to have a field with a tbaa that doesn't alias it's containing struct's tbaa - //Does the fact that this is marked as constant make this fine? + return ctx.alias().constant; + // Specify if this specific field is const or not, which helps with later refinements (especially gc-root-lowering). + if (ai.region == jl_aliasinfo_t::Region::mutfields) + return ai.withRegion(ctx, jl_field_isconst(jt, idx) + ? jl_aliasinfo_t::Region::mutconstdata : jl_aliasinfo_t::Region::mutdata); return ai; } @@ -3663,7 +3666,7 @@ static jl_cgval_t emit_getfield_knownidx(jl_codectx_t &ctx, const jl_cgval_t &st Value *tindex0 = ctx.builder.CreateExtractValue(obj, ArrayRef(ptindex)); Value *tindex = ctx.builder.CreateNUWAdd(ConstantInt::get(getInt8Ty(ctx.builder.getContext()), 1), tindex0); setNameWithField(ctx.emission_context, tindex, get_objname, jt, idx, Twine(".tindex")); - return mark_julia_slot(lv, jfty, tindex, best_aliasinfo(ctx, jfty)); + return mark_julia_slot(lv, jfty, tindex, union_slot_aliasinfo(ctx, jfty)); } else { unsigned st_idx; @@ -4575,8 +4578,9 @@ static jl_cgval_t emit_new_struct(jl_codectx_t &ctx, jl_value_t *ty, size_t narg ssize_t promotion_ssa = -1; Value *strct; // This stack object is a write-once copy of an immutable holding no - // pointers (those are split into inline_roots). - const jl_aliasinfo_t strct_ai = best_aliasinfo(ctx, ty).withRegion(ctx, jl_aliasinfo_t::Region::stack); + // pointers (those are split into inline_roots), and must be eligible for promotion later. + const jl_aliasinfo_t strct_ai = best_aliasinfo(ctx, ty); + assert(strct_ai.region == jl_aliasinfo_t::Region::immutdata); SmallVector inline_roots; if (type_is_ghost(lt)) { strct = nullptr; diff --git a/src/clangsa/GCCheckerReporting.cpp b/src/clangsa/GCCheckerReporting.cpp index 62540565122e7..83c7a13ccb7d1 100644 --- a/src/clangsa/GCCheckerReporting.cpp +++ b/src/clangsa/GCCheckerReporting.cpp @@ -189,7 +189,9 @@ PDP GCChecker::GCValueBugVisitor::VisitNode(const ExplodedNode *N, !isFDAnnotatedNotSafepoint(NewSymbolState->FD, BRC.getSourceManager()); bool maybeUnrooted = declHasAnnotation(NewSymbolState->PVD, "julia_maybe_unrooted"); - assert(isFunctionSafepoint || maybeUnrooted); + // `LivenessState::getForArgument` records FD/PVD only when the function + // is not a safepoint or the parameter is JL_MAYBE_UNROOTED. + assert(!isFunctionSafepoint || maybeUnrooted); (void)maybeUnrooted; Pos = PathDiagnosticLocation{NewSymbolState->PVD, BRC.getSourceManager()}; diff --git a/src/codegen.cpp b/src/codegen.cpp index 64bed6f3fb283..71e9c5c19f33d 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -47,6 +47,7 @@ #include // support +#include #include #include #include @@ -342,29 +343,31 @@ struct jl_typecache_t { namespace { struct jl_tbaacache_t { - // type-based alias analysis nodes. Indentation of comments indicates hierarchy. - MDNode *tbaa_root = nullptr; // Everything - MDNode *tbaa_gcframe = nullptr; // GC frame + // Type-based alias analysis nodes. Indentation of comments indicates hierarchy. + MDNode *tbaa_root = nullptr; // Everything + MDNode *tbaa_gcframe = nullptr; // GC frame // LLVM should have enough info for alias analysis of non-gcframe stack slot - MDNode *tbaa_stack = nullptr; // untyped stack slot which aren't julia data - MDNode *tbaa_data = nullptr; // Any user data that `pointerset/ref` are allowed to alias - MDNode *tbaa_binding = nullptr; // jl_binding_t::value + MDNode *tbaa_stack = nullptr; // Untyped stack slot which aren't julia data + MDNode *tbaa_memoryselbyte = nullptr;// A selector byte in an isbits Union jl_genericmemory_t + MDNode *tbaa_data = nullptr; // Any user data that `pointerset/ref` are allowed to alias + // This distinguishes what kind of slot is accessed: an ordinary field of a + // user-defined layout, or one of the object headers the runtime manages. Whether + // it may be mutated, and by whom, is a property of the memory it lives in, and is + // described by the alias region instead (see `jl_regions_t`). MDNode *tbaa_value = nullptr; // jl_value_t of statically unknown type; parent of the tags below - MDNode *tbaa_mutab = nullptr; // mutable type - MDNode *tbaa_array = nullptr; // jl_array_t header, mutable - MDNode *tbaa_arrayptr = nullptr; // The pointer inside a jl_array_t (to a memoryref) - MDNode *tbaa_arraysize = nullptr; // A size in a jl_array_t - MDNode *tbaa_datatype = nullptr; // datatype - MDNode *tbaa_immut = nullptr; // immutable type + MDNode *tbaa_field = nullptr; // an ordinary field of a user-defined layout + MDNode *tbaa_ptrmemorybuf = nullptr; // a boxed element slot of a jl_genericmemory_t buffer + MDNode *tbaa_binding = nullptr; // jl_binding_t::value + MDNode *tbaa_array = nullptr; // jl_array_t header + MDNode *tbaa_arrayptr = nullptr; // The pointer inside a jl_array_t (to a memoryref) + MDNode *tbaa_arraysize = nullptr; // A size in a jl_array_t + MDNode *tbaa_datatype = nullptr; // jl_datatype_t fields MDNode *tbaa_memory = nullptr; // jl_genericmemory_t header, never mutated after construction - MDNode *tbaa_memoryptr = nullptr; // The pointer inside a jl_genericmemory_t - MDNode *tbaa_memorylen = nullptr; // The length in a jl_genericmemory_t - MDNode *tbaa_memoryown = nullptr; // The owner in a foreign jl_genericmemory_t - MDNode *tbaa_ptrarraybuf = nullptr; // Data in an array of boxed values - MDNode *tbaa_arraybuf = nullptr; // Data in an array of POD - MDNode *tbaa_arrayselbyte = nullptr; // a selector byte in a isbits Union jl_genericmemory_t - MDNode *tbaa_const = nullptr; // Memory that is immutable by the time LLVM can see it - MDNode *tbaa_coverage = nullptr; // Coverage and malloc-log counters; disjoint from all user-visible memory + MDNode *tbaa_memoryptr = nullptr; // The pointer inside a jl_genericmemory_t + MDNode *tbaa_memorylen = nullptr; // The length in a jl_genericmemory_t + MDNode *tbaa_memoryown = nullptr; // The owner in a foreign jl_genericmemory_t + MDNode *tbaa_const = nullptr; // Memory that is immutable by the time this compile unit can access it + MDNode *tbaa_coverage = nullptr; // Coverage and malloc-log counters; disjoint from all user-visible memory bool initialized = false; jl_tbaacache_t() = default; @@ -386,34 +389,24 @@ struct jl_tbaacache_t { tbaa_root = mbuilder.createTBAAScalarTypeNode("jtbaa", jtbaa); tbaa_gcframe = tbaa_make_child(mbuilder, "jtbaa_gcframe").first; tbaa_stack = tbaa_make_child(mbuilder, "jtbaa_stack").first; + tbaa_memoryselbyte = tbaa_make_child(mbuilder, "jtbaa_memoryselbyte").first; MDNode *tbaa_data_scalar; std::tie(tbaa_data, tbaa_data_scalar) = tbaa_make_child(mbuilder, "jtbaa_data"); - tbaa_binding = tbaa_make_child(mbuilder, "jtbaa_binding", tbaa_data_scalar).first; MDNode *tbaa_value_scalar; std::tie(tbaa_value, tbaa_value_scalar) = tbaa_make_child(mbuilder, "jtbaa_value", tbaa_data_scalar); - MDNode *tbaa_mutab_scalar; - std::tie(tbaa_mutab, tbaa_mutab_scalar) = - tbaa_make_child(mbuilder, "jtbaa_mutab", tbaa_value_scalar); - tbaa_datatype = tbaa_make_child(mbuilder, "jtbaa_datatype", tbaa_mutab_scalar).first; - MDNode *tbaa_immut_scalar; - std::tie(tbaa_immut, tbaa_immut_scalar) = - tbaa_make_child(mbuilder, "jtbaa_immut", tbaa_value_scalar); - tbaa_arraybuf = tbaa_make_child(mbuilder, "jtbaa_arraybuf", tbaa_data_scalar).first; - tbaa_ptrarraybuf = tbaa_make_child(mbuilder, "jtbaa_ptrarraybuf", tbaa_data_scalar).first; - // The header tags must descend from jtbaa_value: an array is also reachable - // through a type-erased pointer (a Union or an abstract type, including Any), - // which `best_aliasinfo` tags jtbaa_value, and a disjoint subtree would make that - // load NoAlias against the stores that initialize the header. + tbaa_field = tbaa_make_child(mbuilder, "jtbaa_field", tbaa_value_scalar).first; + tbaa_ptrmemorybuf = tbaa_make_child(mbuilder, "jtbaa_ptrmemorybuf", tbaa_value_scalar).first; + tbaa_binding = tbaa_make_child(mbuilder, "jtbaa_binding", tbaa_value_scalar).first; + tbaa_datatype = tbaa_make_child(mbuilder, "jtbaa_datatype", tbaa_value_scalar).first; MDNode *tbaa_array_scalar; std::tie(tbaa_array, tbaa_array_scalar) = - tbaa_make_child(mbuilder, "jtbaa_array", tbaa_mutab_scalar); + tbaa_make_child(mbuilder, "jtbaa_array", tbaa_value_scalar); tbaa_arrayptr = tbaa_make_child(mbuilder, "jtbaa_arrayptr", tbaa_array_scalar).first; tbaa_arraysize = tbaa_make_child(mbuilder, "jtbaa_arraysize", tbaa_array_scalar).first; - tbaa_arrayselbyte = tbaa_make_child(mbuilder, "jtbaa_arrayselbyte", tbaa_array_scalar).first; MDNode *tbaa_memory_scalar; std::tie(tbaa_memory, tbaa_memory_scalar) = - tbaa_make_child(mbuilder, "jtbaa_memory", tbaa_immut_scalar); + tbaa_make_child(mbuilder, "jtbaa_memory", tbaa_value_scalar); tbaa_memoryptr = tbaa_make_child(mbuilder, "jtbaa_memoryptr", tbaa_memory_scalar).first; tbaa_memorylen = tbaa_make_child(mbuilder, "jtbaa_memorylen", tbaa_memory_scalar).first; tbaa_memoryown = tbaa_make_child(mbuilder, "jtbaa_memoryown", tbaa_memory_scalar).first; @@ -429,12 +422,21 @@ struct jl_noaliascache_t { // "No aliasing" is inferred if it is implied by any domain. // memory regions domain + // + // A region is worth a scope here only if it says something '!tbaa' cannot. + // A private copy of an immutable value is `immutdata` because LLVM cannot express + // this any more accurately on memcpy, and its privacy is visible from its `alloca` address anyways. + // + // Rooting invariant (relied upon by `isLoadFromRootedRegion`): + // if an access's scope set in this domain is contained in + // {jnoalias_immutdata, jnoalias_mutconstdata}, the base object cannot stop + // referencing a tracked pointer stored there while the base is live, even across phi nodes. struct jl_regions_t { MDNode *gcframe = nullptr; // GC frame - MDNode *stack = nullptr; // Stack slot - MDNode *data = nullptr; // Any user data that `pointerset/ref` are allowed to alias - MDNode *constant = nullptr; // Memory that is immutable by the time LLVM can see it - MDNode *coverage = nullptr; // Coverage and malloc-log counters + MDNode *mutdata = nullptr; // The assignable fields of a mutable jl_value_t + MDNode *mutconstdata = nullptr; // The `const` fields of a mutable jl_value_t + MDNode *immutdata = nullptr; // The payload of an immutable jl_value_t + MDNode *memorybuf = nullptr; // The element data of a jl_genericmemory_t jl_regions_t() = default; @@ -443,10 +445,10 @@ struct jl_noaliascache_t { MDNode *domain = mbuilder.createAliasScopeDomain("jnoalias"); this->gcframe = mbuilder.createAliasScope("jnoalias_gcframe", domain); - this->stack = mbuilder.createAliasScope("jnoalias_stack", domain); - this->data = mbuilder.createAliasScope("jnoalias_data", domain); - this->constant = mbuilder.createAliasScope("jnoalias_const", domain); - this->coverage = mbuilder.createAliasScope("jnoalias_coverage", domain); + this->mutdata = mbuilder.createAliasScope("jnoalias_mutdata", domain); + this->mutconstdata = mbuilder.createAliasScope("jnoalias_mutconstdata", domain); + this->immutdata = mbuilder.createAliasScope("jnoalias_immutdata", domain); + this->memorybuf = mbuilder.createAliasScope("jnoalias_memorybuf", domain); } } regions; @@ -466,7 +468,7 @@ struct jl_noaliascache_t { void initialize(llvm::LLVMContext &context) { if (initialized) { - assert(®ions.constant->getContext() == &context); + assert(®ions.gcframe->getContext() == &context); return; } initialized = true; @@ -1716,16 +1718,48 @@ static void union_alloca_type(jl_uniontype_t *ut, // Alias Analysis Info (analogous to llvm::AAMDNodes) // -// This combines the two orthogonal pieces of alias information codegen tracks -// for a memory location: -// - the memory region it lives in (gcframe / stack / heap data / constant), -// expressed as '!alias.scope' + '!noalias' metadata derived from -// jl_noaliascache_t::jl_regions_t, and -// - the layout/type of the data stored there, expressed as struct-path -// '!tbaa' metadata from the jl_tbaacache_t tree. +// This combines the two orthogonal pieces of alias information codegen tracks for a +// memory location: +// - *where* it lives: the memory region (gcframe / mutable object fields / +// immutable object payload / memory buffer), emitted as '!alias.scope' plus +// '!noalias' from jl_noaliascache_t::jl_regions_t, and +// - *how* it is stored: which kind of slot it is, emitted as a struct-path '!tbaa' +// access tag from the jl_tbaacache_t tree. This describes the containing object, +// not the field's own type: a `memcpy` has one tag for both of its operands, so a +// tag that varied with the data could not survive a copy. namespace { struct jl_aliasinfo_t { - enum class Region { unknown, gcframe, stack, data, constant, coverage }; // See jl_regions_t + // The set of memory regions an access may touch: each region in the set is one + // '!alias.scope' scope, and every region outside it goes in '!noalias'. + // The empty set (`unknown`) adds no region metadata. + enum class Region : uint8_t { + unknown = 0, + // GC-frame is leaked globally after final-gc-lowering pass. + gcframe = 1 << 0, + // The assignable fields of a heap-allocated jl_value_t, notably including + // `Array`'s header: the memory a generic `setfield!` writes. + mutdata = 1 << 1, + // The `const` fields of a mutable jl_value_t, including the `GenericMemory` + // header and `DataType`'s fields. A `setfield!` can never reach these, so + // what they hold stays reachable for as long as the object is live -- which + // is what lets late-gc-lowering root through them. + mutconstdata = 1 << 2, + // The payload of an immutable jl_value_t: on the heap, or in a codegen-private + // write-once copy on the stack. Both are the same value and neither is + // writable once built, so a region has nothing to tell them apart by. + immutdata = 1 << 3, + // The element data of a jl_genericmemory_t. An inline element is an immutable + // value too, but the slot holding it is not: `setindex!` overwrites it in + // place, so unlike `immutdata` this cannot be rooted through. + memorybuf = 1 << 4, + // Any field of a mutable jl_value_t. + mutfields = mutdata | mutconstdata, + // Any field of any jl_value_t. + data = mutfields | immutdata, + // Everything a raw `Ptr` may legally read from our alias regions. + anydata = data | memorybuf, + LLVM_MARK_AS_BITMASK_ENUM(memorybuf) + }; MDNode *tbaa = nullptr; // '!tbaa': Struct-path TBAA. TBAA graph forms a tree (indexed by offset). // Two pointers do not alias if they are not transitive parents @@ -1748,16 +1782,26 @@ struct jl_aliasinfo_t { return tbaa != nullptr; } - // The same layout tag in a different memory region: e.g. a private + // The same access tag in a different memory region: e.g. a private // stack copy of typed data keeps the type's `!tbaa`, while the region // records that it is not heap memory. jl_aliasinfo_t withRegion(jl_codectx_t &ctx, Region r) const { return jl_aliasinfo_t(ctx, r, this->tbaa); } + // Whether the access tag is an immutable one (`jtbaa_const`), meaning nothing + // may store to this memory for as long as this compilation unit can reach it. + // This is the axis constant memory is described on -- it has no region. + bool isConstant() const { + if (!tbaa || tbaa->getNumOperands() < 4) + return false; + auto *imm = mdconst::dyn_extract(tbaa->getOperand(3)); + return imm && !imm->isZero(); + } + // Add !tbaa, !tbaa.struct, !alias.scope, !noalias annotations to an instruction. // - // Also adds `invariant.load` to load instructions from the constant region. + // Also adds `invariant.load` to loads of constant memory. Instruction *decorateInst(Instruction *inst) const { if (this->tbaa) @@ -1769,8 +1813,8 @@ struct jl_aliasinfo_t { if (this->noalias) inst->setMetadata(LLVMContext::MD_noalias, this->noalias); - // If this is in the read-only region, mark the load with "!invariant.load" - if (this->region == Region::constant && isa(inst)) + // If this memory is read-only, mark the load with "!invariant.load" + if (this->isConstant() && isa(inst)) inst->setMetadata(LLVMContext::MD_invariant_load, MDNode::get(inst->getContext(), {})); return inst; @@ -1778,7 +1822,7 @@ struct jl_aliasinfo_t { // Merge two sets of alias information, for an instruction (such as a memcpy) // that accesses both. - jl_aliasinfo_t merge(jl_codectx_t &ctx, const jl_aliasinfo_t &other) const; + jl_aliasinfo_t merge(const jl_aliasinfo_t &other) const; AAMDNodes toAAMDNodes() const { @@ -1793,30 +1837,25 @@ struct jl_aliasinfo_t { // Pre-combined alias info for common memory locations referenced by // codegen. Access lazily via `ctx.alias()`. struct jl_aliascache_t { - // Region::gcframe jl_aliasinfo_t gcframe; // GC frame - // Region::stack jl_aliasinfo_t stack; // untyped stack slot - // Region::data jl_aliasinfo_t data; // Any user data that `pointerset/ref` are allowed to alias - jl_aliasinfo_t binding; // jl_binding_t::value jl_aliasinfo_t value; // jl_value_t of statically unknown type - jl_aliasinfo_t mutab; // mutable type + jl_aliasinfo_t immut; // the payload of an immutable jl_value_t + jl_aliasinfo_t mutab; // the fields of a mutable jl_value_t + jl_aliasinfo_t binding; // jl_binding_t::value jl_aliasinfo_t datatype; // datatype - jl_aliasinfo_t immut; // immutable type - jl_aliasinfo_t ptrarraybuf; // Data in an array of boxed values - jl_aliasinfo_t arraybuf; // Data in an array of POD jl_aliasinfo_t array; // jl_array_t header jl_aliasinfo_t arrayptr; // The pointer inside a jl_array_t (to a memoryref) jl_aliasinfo_t arraysize; // A size in a jl_array_t - jl_aliasinfo_t arrayselbyte; // a selector byte in an isbits Union jl_genericmemory_t jl_aliasinfo_t memory; // jl_genericmemory_t header jl_aliasinfo_t memoryptr; // The pointer inside a jl_genericmemory_t jl_aliasinfo_t memorylen; // The length in a jl_genericmemory_t jl_aliasinfo_t memoryown; // The owner in a foreign jl_genericmemory_t - // Region::constant - jl_aliasinfo_t constant; // Memory that is immutable by the time LLVM can see it - // Region::coverage + jl_aliasinfo_t memorybuf; // Inline elements of a jl_genericmemory_t + jl_aliasinfo_t ptrmemorybuf; // Boxed element slots of a jl_genericmemory_t + jl_aliasinfo_t memoryselbyte; // a selector byte in an isbits Union jl_genericmemory_t + jl_aliasinfo_t constant; // Memory that is immutable by the time this compile unit can access it jl_aliasinfo_t coverage; // Coverage and malloc-log counters bool initialized = false; @@ -2200,62 +2239,46 @@ static void jl_temporary_root(jl_codegen_output_t &ctx, jl_value_t *val) JL_CANS static void jl_temporary_root(jl_codectx_t &ctx, jl_value_t *val) JL_CANSAFEPOINT; jl_aliasinfo_t::jl_aliasinfo_t(jl_codectx_t &ctx, Region r, MDNode *tbaa): tbaa(tbaa), tbaa_struct(nullptr), region(r) { - MDNode *alias_scope = nullptr; jl_noaliascache_t::jl_regions_t regions = ctx.noalias().regions; - switch (r) { - case Region::unknown: - alias_scope = nullptr; - break; - case Region::gcframe: - alias_scope = regions.gcframe; - break; - case Region::stack: - alias_scope = regions.stack; - break; - case Region::data: - alias_scope = regions.data; - break; - case Region::constant: - alias_scope = regions.constant; - break; - case Region::coverage: - alias_scope = regions.coverage; - break; + std::pair all_scopes[] = { + { Region::gcframe, regions.gcframe }, + { Region::mutdata, regions.mutdata }, + { Region::mutconstdata, regions.mutconstdata }, + { Region::immutdata, regions.immutdata }, + { Region::memorybuf, regions.memorybuf }, + }; + // The regions in the set are added to !alias.scope, all others to !noalias + SmallVector scopes; + SmallVector noaliases; + for (auto const ®ion: all_scopes) { + if ((r & region.first) != Region::unknown) + scopes.push_back(region.second); + else + noaliases.push_back(region.second); } - - MDNode *all_scopes[] = { regions.gcframe, regions.stack, regions.data, regions.constant, - regions.coverage }; - if (alias_scope) { - // The matching region is added to !alias.scope - // All other regions are added to !noalias - - SmallVector noaliases; - for (MDNode *scope : all_scopes) { - if (scope == alias_scope) continue; - noaliases.push_back(scope); - } - - Metadata *scopes[1] = { alias_scope }; - this->scope = MDNode::get(ctx.builder.getContext(), ArrayRef(scopes)); + if (!scopes.empty()) { + this->scope = MDNode::get(ctx.builder.getContext(), scopes); this->noalias = MDNode::get(ctx.builder.getContext(), noaliases); } } -jl_aliasinfo_t jl_aliasinfo_t::merge(jl_codectx_t &ctx, const jl_aliasinfo_t &other) const { +jl_aliasinfo_t jl_aliasinfo_t::merge(const jl_aliasinfo_t &other) const { + // Constant memory is never stored to, and lives in no region, so an access that + // also touches it can never be the subject of an aliasing query whose answer it + // changes: keep the other side's claim whole, instead of climbing to the (much + // coarser) common parent tag. + if (this->isConstant() || other.isConstant()) { + jl_aliasinfo_t result = this->isConstant() ? other : *this; + result.tbaa_struct = nullptr; + return result; + } jl_aliasinfo_t result; - // Nothing may store to `jtbaa_const` memory, so an access that also touches it - // is never the subject of a TBAA query whose answer could change: keep the other - // side's precise tag instead of climbing to their (much coarser) common parent. - if (this->tbaa == ctx.tbaa().tbaa_const) - result.tbaa = other.tbaa; - else if (other.tbaa == ctx.tbaa().tbaa_const) - result.tbaa = this->tbaa; - else - result.tbaa = MDNode::getMostGenericTBAA(this->tbaa, other.tbaa); + result.tbaa = MDNode::getMostGenericTBAA(this->tbaa, other.tbaa); result.tbaa_struct = nullptr; result.scope = MDNode::getMostGenericAliasScope(this->scope, other.scope); result.noalias = MDNode::intersect(this->noalias, other.noalias); - result.region = this->region == other.region ? this->region : Region::unknown; + result.region = (this->region == Region::unknown || other.region == Region::unknown) + ? Region::unknown : (this->region | other.region); return result; } @@ -2269,50 +2292,64 @@ void jl_aliascache_t::initialize(jl_codectx_t &ctx) auto &tbaa = ctx.tbaa(); using Region = jl_aliasinfo_t::Region; gcframe = jl_aliasinfo_t(ctx, Region::gcframe, tbaa.tbaa_gcframe); - stack = jl_aliasinfo_t(ctx, Region::stack, tbaa.tbaa_stack); - data = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_data); - binding = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_binding); + stack = jl_aliasinfo_t(ctx, Region::unknown, tbaa.tbaa_stack); + data = jl_aliasinfo_t(ctx, Region::anydata, tbaa.tbaa_data); + binding = jl_aliasinfo_t(ctx, Region::mutdata, tbaa.tbaa_binding); value = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_value); - mutab = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_mutab); - datatype = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_datatype); - immut = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_immut); - ptrarraybuf = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_ptrarraybuf); - arraybuf = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_arraybuf); - array = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_array); - arrayptr = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_arrayptr); - arraysize = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_arraysize); - arrayselbyte = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_arrayselbyte); - memory = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_memory); - memoryptr = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_memoryptr); - memorylen = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_memorylen); - memoryown = jl_aliasinfo_t(ctx, Region::data, tbaa.tbaa_memoryown); - constant = jl_aliasinfo_t(ctx, Region::constant, tbaa.tbaa_const); - coverage = jl_aliasinfo_t(ctx, Region::coverage, tbaa.tbaa_coverage); -} - -// Alias info for the inline data of an `sret` return buffer. Both the caller -// and the callee compute this from the return type, so that the two sides' -// alias info for the same memory stays identical if LLVM later inlines the call. + mutab = jl_aliasinfo_t(ctx, Region::mutfields, tbaa.tbaa_field); + immut = jl_aliasinfo_t(ctx, Region::immutdata, tbaa.tbaa_field); + datatype = jl_aliasinfo_t(ctx, Region::mutconstdata, tbaa.tbaa_datatype); + array = jl_aliasinfo_t(ctx, Region::mutdata, tbaa.tbaa_array); + arrayptr = jl_aliasinfo_t(ctx, Region::mutdata, tbaa.tbaa_arrayptr); + arraysize = jl_aliasinfo_t(ctx, Region::mutdata, tbaa.tbaa_arraysize); + memory = jl_aliasinfo_t(ctx, Region::mutconstdata, tbaa.tbaa_memory); + memoryptr = jl_aliasinfo_t(ctx, Region::mutconstdata, tbaa.tbaa_memoryptr); + memorylen = jl_aliasinfo_t(ctx, Region::mutconstdata, tbaa.tbaa_memorylen); + memoryown = jl_aliasinfo_t(ctx, Region::mutconstdata, tbaa.tbaa_memoryown); + memorybuf = jl_aliasinfo_t(ctx, Region::memorybuf, tbaa.tbaa_field); + ptrmemorybuf = jl_aliasinfo_t(ctx, Region::memorybuf, tbaa.tbaa_ptrmemorybuf); + memoryselbyte = jl_aliasinfo_t(ctx, Region::memorybuf, tbaa.tbaa_memoryselbyte); + constant = jl_aliasinfo_t(ctx, Region::unknown, tbaa.tbaa_const); + coverage = jl_aliasinfo_t(ctx, Region::unknown, tbaa.tbaa_coverage); +} + +// Alias info for the inline data of a type. static jl_aliasinfo_t best_aliasinfo(jl_codectx_t &ctx, jl_value_t *jt); +static jl_aliasinfo_t union_slot_aliasinfo(jl_codectx_t &ctx, jl_value_t *typ); static jl_aliasinfo_t sret_aliasinfo(jl_codectx_t &ctx, jl_value_t *jlretty, bool all_roots) { if (all_roots) return ctx.alias().gcframe; // A union return's payload pointer is a select of the buffer and a box. if (!jl_is_concrete_type(jlretty)) - return best_aliasinfo(ctx, jlretty); + return union_slot_aliasinfo(ctx, jlretty); // Otherwise the buffer holds no tracked pointers (those travel in the separate - // return_roots buffer), so this write-once copy keeps the return type's layout - // tag, with the region recording that it is not heap memory. - return best_aliasinfo(ctx, jlretty).withRegion(ctx, jl_aliasinfo_t::Region::stack); + // return_roots buffer), so it is just another copy of the return value. + const jl_aliasinfo_t ret_ai = best_aliasinfo(ctx, jlretty); + assert(ret_ai.region == jl_aliasinfo_t::Region::immutdata); + return ret_ai; +} +// Alias info for the contents of a union slot that codegen materialized (a phi, a +// local variable of union type, or an `sret` return buffer). Such a slot is addressed +// by a `select` of a private buffer and a box, but is only ever dereferenced for the +// union's inline members, which are immutable by construction -- so `immutdata` is +// a correct refinement. The rooting invariant at `jl_regions_t` permits it too because +// the buffer will not hold tracked pointers (those are managed separately). +// TODO: examine each element of `typ` to build a more precise tag. +static jl_aliasinfo_t union_slot_aliasinfo(jl_codectx_t &ctx, jl_value_t *typ) +{ + (void)typ; + return ctx.alias().immut; } -// Alias info for a private, write-once stack copy of data described by `src_ai`. -// The copy keeps the source's layout tag, but is also known to move to the stack. -static jl_aliasinfo_t stack_copy_aliasinfo(jl_codectx_t &ctx, const jl_aliasinfo_t &src_ai, jl_value_t *typ) +// Alias info for a private, write-once copy of the data `src_ai` describes: the same +// access tag, re-homed in `immutdata`. +static jl_aliasinfo_t private_copy_aliasinfo(jl_codectx_t &ctx, const jl_aliasinfo_t &src_ai, jl_value_t *typ) { - return (src_ai && src_ai.region != jl_aliasinfo_t::Region::constant ? src_ai : best_aliasinfo(ctx, typ)) - .withRegion(ctx, jl_aliasinfo_t::Region::stack); + // Fall back to the type when there is no tag to inherit (a register aggregate) or + // it is `jtbaa_const`, which would claim the stores filling this copy cannot exist. + return (src_ai && !src_ai.isConstant() ? src_ai : best_aliasinfo(ctx, typ)) + .withRegion(ctx, jl_aliasinfo_t::Region::immutdata); } // Select the best (most precise) alias info for a value of the given julia @@ -2334,11 +2371,23 @@ static jl_aliasinfo_t best_aliasinfo(jl_codectx_t &ctx, jl_value_t *jt) return alias.memory; if (jl_is_array_type(jt)) return alias.array; + if (jt == (jl_value_t*)jl_binding_type) + return alias.binding; // If we're here, we know all subtypes are (im)mutable, even if we // don't know what the exact type is return jl_is_mutable(jt) ? alias.mutab : alias.immut; } +// Alias info for the element data of a `jl_genericmemory_t` with this layout. +// This helps with aiding vectorization in seeing the mutable store to the memory +// is disjoint from the reads of all the other objects (such as the array header fields). +static jl_aliasinfo_t memorybuf_aliasinfo(jl_codectx_t &ctx, const jl_datatype_layout_t *layout) +{ + if (layout->flags.arrayelem_isboxed) + return ctx.alias().ptrmemorybuf; + return ctx.alias().memorybuf; +} + static Type *julia_type_to_llvm(jl_codectx_t &ctx, jl_value_t *jt, bool *isboxed = NULL) JL_CANSAFEPOINT; static jl_cgval_t emit_expr(jl_codectx_t &ctx, jl_value_t *expr, ssize_t ssaval = -1) JL_CANSAFEPOINT; static jl_cgval_t emit_checked_var(jl_codectx_t &ctx, Value *bp, jl_sym_t *name, jl_value_t *scope, bool isvol, const jl_aliasinfo_t &ai, AtomicOrdering order=AtomicOrdering::Unordered) JL_CANSAFEPOINT; @@ -2626,15 +2675,10 @@ static inline jl_cgval_t value_to_pointer(jl_codectx_t &ctx, Value *v, jl_value_ Value *loc; v = zext_struct(ctx, v); Align align(julia_alignment(typ)); - // This write-once copy of pointer-free typed data keeps the type's prior layout tag. - // However, there is a subtlety for pointer-containing copies: llvm-late-gc-lowering - // does not correctly check for dominator for alloca when using it as root, so it could - // use it as a refinement base past the time the alloca has already been reused. Since - // this is pretty much the only time that should matter, we currently hack around that - // here rather than fixing it correctly for now. - jl_aliasinfo_t ai = (tindex == nullptr && jl_is_pointerfree(typ) ? - best_aliasinfo(ctx, typ) : ctx.alias().value) - .withRegion(ctx, jl_aliasinfo_t::Region::stack); + // A write-once copy of the value, so it keeps the type's access tag and the + // `immutdata` region the value has anywhere else. + jl_aliasinfo_t ai = best_aliasinfo(ctx, typ) + .withRegion(ctx, jl_aliasinfo_t::Region::immutdata); if (valid_as_globalinit(v)) { // llvm can't handle all the things that could be inside a ConstantExpr assert(jl_is_concrete_type(typ)); // not legal to have an unboxed abstract type loc = get_pointer_to_constant(ctx.emission_context, cast(v), align, "_j_const", *jl_Module); @@ -2676,9 +2720,11 @@ static inline jl_cgval_t value_to_pointer(jl_codectx_t &ctx, const jl_cgval_t &v Constant::getNullValue(ctx.types().T_prjlvalue), ptr_field, Align(sizeof(void *))); } } - jl_aliasinfo_t stack_ai = v.V == nullptr ? ctx.alias().gcframe : best_aliasinfo(ctx, v.typ); - recombine_value(ctx, v, loc, stack_ai, align, false); - return mark_julia_slot(loc, v.typ, v.TIndex, stack_ai); + // The pointers written back here are a silent copy of the ones the separate + // roots buffer holds; this buffer is never itself a root. + jl_aliasinfo_t slot_ai = private_copy_aliasinfo(ctx, v.aliasinfo, v.typ); + recombine_value(ctx, v, loc, slot_ai, align, false); + return mark_julia_slot(loc, v.typ, v.TIndex, slot_ai); } if (v.ispointer() || v.V == nullptr) return v; @@ -4215,7 +4261,7 @@ static Value *emit_bits_compare(jl_codectx_t &ctx, const jl_cgval_t &arg1, const ai = parg1.aliasinfo; } else { - ai = parg1.aliasinfo.merge(ctx, parg2.aliasinfo); + ai = parg1.aliasinfo.merge(parg2.aliasinfo); } ai.decorateInst(answer); } @@ -4635,7 +4681,7 @@ static bool emit_f_opmemory(jl_codectx_t &ctx, jl_cgval_t *ret, jl_value_t *f, // parent (the memory object), so a NULL child is fine. emit_write_barrier(ctx, mem, Constant::getNullValue(ctx.types().T_prjlvalue)); emit_aliased_store(ctx, Constant::getNullValue(elty), ptr, Align(al), - isboxed ? ctx.alias().ptrarraybuf : ctx.alias().arraybuf, + memorybuf_aliasinfo(ctx, layout), ctx.noalias().aliasscope.current, storeOrder); if (lock) emit_lockstate_value(ctx, lock, false); @@ -4681,7 +4727,7 @@ static bool emit_f_opmemory(jl_codectx_t &ctx, jl_cgval_t *ret, jl_value_t *f, } ptindex = emit_ptrgep(ctx, ptindex, idx0); ptr = data; - ai_ptindex = ctx.alias().arrayselbyte; + ai_ptindex = ctx.alias().memoryselbyte; } else { ptr = (layout->size == 0 ? nullptr : emit_memoryref_ptr(ctx, ref, layout)); @@ -4698,7 +4744,7 @@ static bool emit_f_opmemory(jl_codectx_t &ctx, jl_cgval_t *ret, jl_value_t *f, *ret = typed_store(ctx, ptr, val, cmp, ety, - isboxed ? ctx.alias().ptrarraybuf : ctx.alias().arraybuf, + memorybuf_aliasinfo(ctx, layout), isunion ? nullptr : ctx.noalias().aliasscope.current, data_owner, isboxed, @@ -5070,8 +5116,8 @@ static bool emit_builtin_call(jl_codectx_t &ctx, jl_cgval_t *ret, jl_value_t *f, data = ctx.builder.CreateInBoundsGEP(AT, data, idx0); } ptindex = emit_ptrgep(ctx, ptindex, idx0); - *ret = typed_load(ctx, data, NULL, ety, ctx.alias().arraybuf, nullptr, false, - AtomicOrdering::NotAtomic, false, 0, nullptr, ptindex, ctx.alias().arrayselbyte); + *ret = typed_load(ctx, data, NULL, ety, memorybuf_aliasinfo(ctx, layout), nullptr, false, + AtomicOrdering::NotAtomic, false, 0, nullptr, ptindex, ctx.alias().memoryselbyte); } else { Value *ptr = (layout->size == 0 ? nullptr : emit_memoryref_ptr(ctx, ref, layout)); @@ -5084,7 +5130,7 @@ static bool emit_builtin_call(jl_codectx_t &ctx, jl_cgval_t *ret, jl_value_t *f, emit_lockstate_value(ctx, lock, true); } *ret = typed_load(ctx, ptr, nullptr, ety, - isboxed ? ctx.alias().ptrarraybuf : ctx.alias().arraybuf, + memorybuf_aliasinfo(ctx, layout), ctx.noalias().aliasscope.current, isboxed, Order, maybenull, al); if (needlock) { @@ -5176,7 +5222,7 @@ static bool emit_builtin_call(jl_codectx_t &ctx, jl_cgval_t *ret, jl_value_t *f, elem = emit_ptrgep(ctx, elem, LLT_ALIGN(sizeof(jl_mutex_t), JL_SMALL_BYTE_ALIGNMENT)); // emit this using the same type as BUILTIN(memoryrefget) // so that LLVM may be able to load-load forward them and fold the result - jl_aliasinfo_t ai = isboxed ? ctx.alias().ptrarraybuf : ctx.alias().arraybuf; + jl_aliasinfo_t ai = memorybuf_aliasinfo(ctx, layout); LoadInst *fldv = ctx.builder.CreateAlignedLoad(ctx.types().T_prjlvalue, elem, ctx.types().alignof_ptr); fldv->setOrdering(Order); ai.decorateInst(fldv); @@ -6143,7 +6189,7 @@ static jl_cgval_t emit_call_specfun_other(jl_codectx_t &ctx, bool is_opaque_clos retval = mark_julia_slot(derived, jlretty, tindex, - best_aliasinfo(ctx, jlretty), + sret_aliasinfo(ctx, jlretty, returninfo.all_roots), make_lazy_gc_roots(return_roots, returninfo.return_roots, ctx.alias().gcframe)); retval.Vboxed = box; break; @@ -6736,7 +6782,7 @@ static void emit_phinode_assign(jl_codectx_t &ctx, ssize_t idx, jl_value_t *r) J if (dest || allunbox || inline_roots) { Value *ptr = nullptr; PHINode *ptr_phi = nullptr; - jl_aliasinfo_t ai = best_aliasinfo(ctx, phiType); + jl_aliasinfo_t ai = union_slot_aliasinfo(ctx, phiType); PHINode *Tindex_phi = PHINode::Create(getInt8Ty(ctx.builder.getContext()), jl_array_nrows(edges), "tindex_phi"); Tindex_phi->insertInto(BB, InsertPt); if (inline_roots) { @@ -9778,7 +9824,7 @@ static jl_llvm_functions_t Value *lv = try_emit_union_alloca(ctx, (jl_uniontype_t*)jt, allunbox, align, nbytes, inline_roots); if (lv) { lv->setName(jl_symbol_name(s)); - varinfo.value = mark_julia_slot(lv, jt, NULL, best_aliasinfo(ctx, jt)); + varinfo.value = mark_julia_slot(lv, jt, NULL, union_slot_aliasinfo(ctx, jt)); varinfo.pTIndex = emit_static_alloca(ctx, 1, Align(1)); setName(ctx.emission_context, varinfo.pTIndex, "tindex"); // TODO: attach debug metadata to this variable @@ -9927,7 +9973,10 @@ static jl_llvm_functions_t if (isboxed) maybe_mark_argument_dereferenceable(param, argType); theArg = mark_julia_type(ctx, Arg, isboxed, argType); - if (theArg.aliasinfo.tbaa == ctx.tbaa().tbaa_immut) + // The caller roots an immutable argument for the whole call and nothing + // may write its payload while we hold it, so the callee may read it as + // constant memory. + if (theArg.aliasinfo.region == jl_aliasinfo_t::Region::immutdata) theArg.aliasinfo = ctx.alias().constant; } attrs[Arg->getArgNo()] = AttributeSet::get(Arg->getContext(), param); // function declaration attributes @@ -10780,7 +10829,7 @@ static jl_llvm_functions_t else if (jl_is_concrete_type(val.typ) || val.constant) { size_t tindex = get_box_tindex((jl_datatype_t*)(val.constant ? jl_typeof(val.constant) : val.typ), phiType); if (tindex && dest && (!VN || !val.isboxed)) { - emit_unionmove(ctx, dest, phiType, best_aliasinfo(ctx, phiType), val, RTindex, nullptr); + emit_unionmove(ctx, dest, phiType, union_slot_aliasinfo(ctx, phiType), val, RTindex, nullptr); } } else { @@ -10796,7 +10845,7 @@ static jl_llvm_functions_t ConstantInt::get(getInt8Ty(ctx.builder.getContext()), 0)); skip = ctx.builder.CreateOr(isboxed, skip); } - emit_unionmove(ctx, dest, phiType, best_aliasinfo(ctx, phiType), val, tindex, skip); + emit_unionmove(ctx, dest, phiType, union_slot_aliasinfo(ctx, phiType), val, tindex, skip); } } assert(new_union.inline_roots.size() <= lroots.size()); diff --git a/src/intrinsics.cpp b/src/intrinsics.cpp index a05d945518386..f6c4be5205466 100644 --- a/src/intrinsics.cpp +++ b/src/intrinsics.cpp @@ -518,7 +518,9 @@ static Value *emit_unbox(jl_codectx_t &ctx, Type *to, const jl_cgval_t &x, Maybe std::string type_str = jl_is_datatype(x.typ) ? jl_symbol_name(((jl_datatype_t*)x.typ)->name->name) : ""; return "unbox::" + type_str; }); - auto combined_ai = best_aliasinfo(ctx, x.typ); + // The pointers written back here are a silent copy of the ones the separate + // roots buffer holds; this buffer is never itself a root. + auto combined_ai = private_copy_aliasinfo(ctx, x.aliasinfo, x.typ); recombine_value(ctx, x, combined, combined_ai, alignment, false); p = combined; ai = combined_ai; @@ -1010,14 +1012,13 @@ static jl_cgval_t emit_atomic_pointerref(jl_codectx_t &ctx, ArrayRef setName(ctx.emission_context, strct, "atomic_pointerref_box"); Value *thePtr = emit_unbox(ctx, getPointerTy(ctx.builder.getContext()), e); Type *loadT = Type::getIntNTy(ctx.builder.getContext(), nb * 8); - jl_aliasinfo_t ai = best_aliasinfo(ctx, ety); LoadInst *load = ctx.builder.CreateAlignedLoad(loadT, thePtr, Align(nb)); setName(ctx.emission_context, load, "atomic_pointerref"); - ai.decorateInst(load); + ctx.alias().data.decorateInst(load); load->setOrdering(llvm_order); thePtr = strct; StoreInst *store = ctx.builder.CreateAlignedStore(load, thePtr, Align(julia_alignment(ety))); - ai.decorateInst(store); + best_aliasinfo(ctx, ety).decorateInst(store); return mark_julia_type(ctx, strct, true, ety); } else { @@ -1296,7 +1297,7 @@ static jl_cgval_t emit_ifelse(jl_codectx_t &ctx, jl_cgval_t c, jl_cgval_t x, jl_ y_ptr = decay_derived(ctx, y_ptr); ifelse_result = ctx.builder.CreateSelect(isfalse, y_ptr, x_ptr); setName(ctx.emission_context, ifelse_result, "ifelse_result"); - ifelse_ai = x.aliasinfo.merge(ctx, y.aliasinfo); + ifelse_ai = x.aliasinfo.merge(y.aliasinfo); if (ifelse_ai.tbaa == NULL) { // LLVM won't return a TBAA result for the root, but mark_julia_slot requires it: make it now auto *OffsetNode = ConstantAsMetadata::get(ConstantInt::get(getInt64Ty(ctx.builder.getContext()), 0)); diff --git a/src/llvm-codegen-shared.h b/src/llvm-codegen-shared.h index 46868194cf19b..3536465a40e05 100644 --- a/src/llvm-codegen-shared.h +++ b/src/llvm-codegen-shared.h @@ -182,16 +182,59 @@ static inline bool isTBAA(llvm::MDNode *TBAA, std::initializer_list return false; } -// Check if this is a load from an immutable value. The easiest way to do so is -// to look at the AA metadata and see if it derives from jtbaa_immut. -static inline bool isLoadFromImmut(llvm::LoadInst *LI) +// The '!alias.scope' domain naming which of codegen's memory regions an access may touch. +#define JL_REGION_DOMAIN_NAME "jnoalias" + +// The regions in whose domain a base object cannot stop referencing a tracked +// pointer stored in them while the base is live. +static inline bool isRootedRegionName(llvm::StringRef name) { - if (LI->getMetadata(llvm::LLVMContext::MD_invariant_load)) - return true; - llvm::MDNode *TBAA = LI->getMetadata(llvm::LLVMContext::MD_tbaa); - if (isTBAA(TBAA, {"jtbaa_immut", "jtbaa_const", "jtbaa_datatype"})) + return name == "jnoalias_immutdata" || name == "jnoalias_mutconstdata"; +} + +// Whether the object rooting the address `LI` loads from also roots the loaded value +// -- so that late-gc-lowering may refine the loaded pointer to the load's pointer +// operand instead of giving it a gc-frame slot of its own. +// +// This asks whether the slot is ever overwritten, so that what the base references +// here is fixed for as long as it lives. The region records that ('!alias.scope'), the +// access tag ('!tbaa') does not. A load qualifies if its scopes in the +// region domain are nonempty and all rooted. +static inline bool isLoadFromRootedRegion(llvm::LoadInst *LI) +{ + using namespace llvm; + // Constant memory never changes, so the base can never stop referencing what is + // stored here, wherever it lives. This is also the only leg that fires on foreign + // IR carrying no region metadata. + if (LI->getMetadata(LLVMContext::MD_invariant_load)) return true; - return false; + MDNode *scopes = LI->getMetadata(LLVMContext::MD_alias_scope); + if (!scopes) + return false; + bool found = false; + for (const MDOperand &op : scopes->operands()) { + MDNode *scope = dyn_cast_or_null(op.get()); + if (!scope) + continue; + AliasScopeNode snode(scope); + const MDNode *domain = snode.getDomain(); + if (!domain || domain->getNumOperands() < 1) + continue; + MDString *domain_name = dyn_cast(domain->getOperand(0)); + if (!domain_name || domain_name->getString() != JL_REGION_DOMAIN_NAME) + continue; + // A scope is named either by its string key in operand 0 ({name, domain}) + // or, when a self-reference keys it, by a trailing name operand + // ({self, domain, name}); AliasScopeNode reads the latter. + StringRef name = snode.getName(); + if (name.empty()) + if (MDString *key = dyn_cast(scope->getOperand(0))) + name = key->getString(); + if (name.empty() || !isRootedRegionName(name)) + return false; // may reside in a region that can drop the reference + found = true; + } + return found; } static inline bool isConstGV(llvm::GlobalVariable *gv) diff --git a/src/llvm-late-gc-lowering.cpp b/src/llvm-late-gc-lowering.cpp index f848f5c749a6d..9d0596e9f8318 100644 --- a/src/llvm-late-gc-lowering.cpp +++ b/src/llvm-late-gc-lowering.cpp @@ -947,7 +947,7 @@ static bool isLoadFromConstGV(LoadInst *LI, bool &task_local, PhiSet *seen) auto load_base = LI->getPointerOperand()->stripInBoundsOffsets(); assert(load_base); // Static analyzer auto gv = dyn_cast(load_base); - if (isLoadFromImmut(LI)) { + if (isLoadFromRootedRegion(LI)) { if (gv) return true; return isLoadFromConstGV(load_base, task_local, seen); @@ -1360,7 +1360,7 @@ State LateLowerGCFrame::LocalScan(Function &F) { continue; } if (LoadInst *LI = dyn_cast(&I)) { - // If this is a load from an immutable, we know that + // If this is a load from a rooted region, we know that // this object will always be rooted as long as the // object we're loading from is, so we can refine uses // of this object to uses of the object we're loading @@ -1369,7 +1369,7 @@ State LateLowerGCFrame::LocalScan(Function &F) { Type *Ty = LI->getType()->getScalarType(); bool refined_globally = false; bool task_local = false; - if (isLoadFromImmut(LI) && isSpecialPtr(LI->getPointerOperand()->getType())) { + if (isLoadFromRootedRegion(LI) && isSpecialPtr(LI->getPointerOperand()->getType())) { RefinedPtr.push_back(Number(S, LI->getPointerOperand())); } else if (isLoadFromConstGV(LI, task_local)) { diff --git a/test/llvmpasses/gcroots.ll b/test/llvmpasses/gcroots.ll index c9a646e88b717..d5876c08067e4 100644 --- a/test/llvmpasses/gcroots.ll +++ b/test/llvmpasses/gcroots.ll @@ -349,9 +349,9 @@ define {} addrspace(10)* @gv_const() { top: %pgcstack = call {}*** @julia.get_pgcstack() %ptls = call {}*** @julia.ptls_states() - %v10 = load {}*, {}** @gv1, !tbaa !2 + %v10 = load {}*, {}** @gv1, !tbaa !2, !invariant.load !3 %v1 = addrspacecast {}* %v10 to {} addrspace(10)* - %v2 = load {} addrspace(10)*, {} addrspace(10)** @gv2, !tbaa !2 + %v2 = load {} addrspace(10)*, {} addrspace(10)** @gv2, !tbaa !2, !invariant.load !3 call void @jl_safepoint() call void @one_arg_boxed({} addrspace(10)* %v1) call void @one_arg_boxed({} addrspace(10)* %v2) @@ -804,3 +804,4 @@ top: !0 = !{!"jtbaa"} !1 = !{!"jtbaa_const", !0, i64 0} !2 = !{!1, !1, i64 0, i64 1} +!3 = !{} diff --git a/test/llvmpasses/refinements.ll b/test/llvmpasses/refinements.ll index 5b277d47319dd..61934185900c9 100644 --- a/test/llvmpasses/refinements.ll +++ b/test/llvmpasses/refinements.ll @@ -16,7 +16,7 @@ define void @argument_refinement({} addrspace(10)* %a) { %pgcstack = call {}*** @julia.get_pgcstack() %ptls = call {}*** @julia.ptls_states() %casted1 = bitcast {} addrspace(10)* %a to {} addrspace(10)* addrspace(10)* - %loaded1 = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %casted1, !tbaa !1 + %loaded1 = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %casted1, !tbaa !1, !alias.scope !7 call void @jl_safepoint() %casted2 = bitcast {} addrspace(10)* %loaded1 to i64 addrspace(10)* %loaded2 = load i64, i64 addrspace(10)* %casted2 @@ -31,7 +31,7 @@ define void @heap_refinement1(i64 %a) { %ptls = call {}*** @julia.ptls_states() %aboxed = call {} addrspace(10)* @ijl_box_int64(i64 signext %a) %casted1 = bitcast {} addrspace(10)* %aboxed to {} addrspace(10)* addrspace(10)* - %loaded1 = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %casted1, !tbaa !1 + %loaded1 = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %casted1, !tbaa !1, !alias.scope !7 ; OPAQUE: store ptr addrspace(10) %aboxed call void @jl_safepoint() %casted2 = bitcast {} addrspace(10)* %loaded1 to i64 addrspace(10)* @@ -48,7 +48,7 @@ define void @heap_refinement2(i64 %a) { %ptls = call {}*** @julia.ptls_states() %aboxed = call {} addrspace(10)* @ijl_box_int64(i64 signext %a) %casted1 = bitcast {} addrspace(10)* %aboxed to {} addrspace(10)* addrspace(10)* - %loaded1 = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %casted1, !tbaa !1 + %loaded1 = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %casted1, !tbaa !1, !alias.scope !7 ; OPAQUE: store ptr addrspace(10) %loaded1 call void @jl_safepoint() %casted2 = bitcast {} addrspace(10)* %loaded1 to i64 addrspace(10)* @@ -64,7 +64,7 @@ define void @issue22770() { %ptls = call {}*** @julia.ptls_states() %y = call {} addrspace(10)* @allocate_some_value() %casted1 = bitcast {} addrspace(10)* %y to {} addrspace(10)* addrspace(10)* - %x = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %casted1, !tbaa !1 + %x = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %casted1, !tbaa !1, !alias.scope !7 ; OPAQUE: store ptr addrspace(10) %y, %a = call {} addrspace(10)* @allocate_some_value() @@ -138,7 +138,7 @@ top: L1: %continue = phi i1 [ true, %top ], [ false, %L1 ] %p = phi {} addrspace(10)* [ %x, %top ], [ %v, %L1 ] - %v0 = load {}*, {}** @gv1, !tbaa !4 + %v0 = load {}*, {}** @gv1, !tbaa !4, !invariant.load !18 %v = addrspacecast {}* %v0 to {} addrspace(10)* call void @one_arg_boxed({} addrspace(10)* %v) call void @one_arg_boxed({} addrspace(10)* %p) @@ -164,7 +164,7 @@ L1: ; of the phi node. Therefore, we need only one gc slot for `%a`. %p = phi {} addrspace(10)* [ %x, %top ], [ %v, %L1 ] %ca = bitcast {} addrspace(10)* %a to {} addrspace(10)* addrspace(10)* - %v = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %ca, !tbaa !1 + %v = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %ca, !tbaa !1, !alias.scope !7 call void @one_arg_boxed({} addrspace(10)* %v) call void @one_arg_boxed({} addrspace(10)* %p) br i1 %continue, label %L1, label %L2 @@ -187,7 +187,7 @@ L1: ; `%p` has circular dependency but it can only be derived from `%a` which dominate `%p`. %p = phi {} addrspace(10)* [ %a, %top ], [ %v, %L1 ] %ca = bitcast {} addrspace(10)* %p to {} addrspace(10)* addrspace(10)* - %v = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %ca, !tbaa !1 + %v = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %ca, !tbaa !1, !alias.scope !7 call void @one_arg_boxed({} addrspace(10)* %v) call void @one_arg_boxed({} addrspace(10)* %p) br i1 %continue, label %L1, label %L2 @@ -223,11 +223,135 @@ define {} addrspace(10)* @setfield({} addrspace(10)* %p) { ret {} addrspace(10)* %c } +; The rooting refinement is keyed on the memory region ('!alias.scope'), not on +; the layout description ('!tbaa'): a content tag that says nothing about +; immutability still refines ... +define void @refine_region_without_type_tag({} addrspace(10)* %a) { +; CHECK-LABEL: @refine_region_without_type_tag +; CHECK-NOT: %gcframe + %pgcstack = call {}*** @julia.get_pgcstack() + %ptls = call {}*** @julia.ptls_states() + %casted1 = bitcast {} addrspace(10)* %a to {} addrspace(10)* addrspace(10)* + %loaded1 = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %casted1, !tbaa !5, !alias.scope !7 + call void @jl_safepoint() + %casted2 = bitcast {} addrspace(10)* %loaded1 to i64 addrspace(10)* + %loaded2 = load i64, i64 addrspace(10)* %casted2 + ret void +} + +; ... and the same layout tag also describes the element data of a `GenericMemory`, +; which may be overwritten while the memory object stays live. +define void @dont_refine_memorybuf_region({} addrspace(10)* %a) { +; CHECK-LABEL: @dont_refine_memorybuf_region +; OPAQUE: %gcframe = alloca ptr addrspace(10), i32 3 + %pgcstack = call {}*** @julia.get_pgcstack() + %ptls = call {}*** @julia.ptls_states() + %casted1 = bitcast {} addrspace(10)* %a to {} addrspace(10)* addrspace(10)* + %loaded1 = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %casted1, !tbaa !1, !alias.scope !10 + call void @jl_safepoint() + %casted2 = bitcast {} addrspace(10)* %loaded1 to i64 addrspace(10)* + %loaded2 = load i64, i64 addrspace(10)* %casted2 + ret void +} + +; An access merged across regions may reside in the unrooted one, so any unrooted +; member of the scope list vetoes the refinement. +define void @dont_refine_merged_region({} addrspace(10)* %a) { +; CHECK-LABEL: @dont_refine_merged_region +; OPAQUE: %gcframe = alloca ptr addrspace(10), i32 3 + %pgcstack = call {}*** @julia.get_pgcstack() + %ptls = call {}*** @julia.ptls_states() + %casted1 = bitcast {} addrspace(10)* %a to {} addrspace(10)* addrspace(10)* + %loaded1 = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %casted1, !tbaa !1, !alias.scope !12 + call void @jl_safepoint() + %casted2 = bitcast {} addrspace(10)* %loaded1 to i64 addrspace(10)* + %loaded2 = load i64, i64 addrspace(10)* %casted2 + ret void +} + +; Scopes from another domain (`@aliasscope`, loop versioning, ...) describe +; something else entirely, even when they happen to share our names. +define void @dont_refine_foreign_domain({} addrspace(10)* %a) { +; CHECK-LABEL: @dont_refine_foreign_domain +; OPAQUE: %gcframe = alloca ptr addrspace(10), i32 3 + %pgcstack = call {}*** @julia.get_pgcstack() + %ptls = call {}*** @julia.ptls_states() + %casted1 = bitcast {} addrspace(10)* %a to {} addrspace(10)* addrspace(10)* + %loaded1 = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %casted1, !tbaa !1, !alias.scope !15 + call void @jl_safepoint() + %casted2 = bitcast {} addrspace(10)* %loaded1 to i64 addrspace(10)* + %loaded2 = load i64, i64 addrspace(10)* %casted2 + ret void +} + +; No region metadata at all is no information, so we conservatively re-root. +define void @dont_refine_without_region({} addrspace(10)* %a) { +; CHECK-LABEL: @dont_refine_without_region +; OPAQUE: %gcframe = alloca ptr addrspace(10), i32 3 + %pgcstack = call {}*** @julia.get_pgcstack() + %ptls = call {}*** @julia.ptls_states() + %casted1 = bitcast {} addrspace(10)* %a to {} addrspace(10)* addrspace(10)* + %loaded1 = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %casted1, !tbaa !1 + call void @jl_safepoint() + %casted2 = bitcast {} addrspace(10)* %loaded1 to i64 addrspace(10)* + %loaded2 = load i64, i64 addrspace(10)* %casted2 + ret void +} + +; A `const` field of a mutable object cannot be reassigned either, so it roots what +; it holds just as an immutable's payload does. +define void @refine_mutconst_region({} addrspace(10)* %a) { +; CHECK-LABEL: @refine_mutconst_region +; CHECK-NOT: %gcframe + %pgcstack = call {}*** @julia.get_pgcstack() + %ptls = call {}*** @julia.ptls_states() + %casted1 = bitcast {} addrspace(10)* %a to {} addrspace(10)* addrspace(10)* + %loaded1 = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %casted1, !tbaa !1, !alias.scope !19 + call void @jl_safepoint() + %casted2 = bitcast {} addrspace(10)* %loaded1 to i64 addrspace(10)* + %loaded2 = load i64, i64 addrspace(10)* %casted2 + ret void +} + +; Foreign IR carrying no region metadata still refines through `!invariant.load`. +define void @refine_invariant_load({} addrspace(10)* %a) { +; CHECK-LABEL: @refine_invariant_load +; CHECK-NOT: %gcframe + %pgcstack = call {}*** @julia.get_pgcstack() + %ptls = call {}*** @julia.ptls_states() + %casted1 = bitcast {} addrspace(10)* %a to {} addrspace(10)* addrspace(10)* + %loaded1 = load {} addrspace(10)*, {} addrspace(10)* addrspace(10)* %casted1, !invariant.load !18 + call void @jl_safepoint() + %casted2 = bitcast {} addrspace(10)* %loaded1 to i64 addrspace(10)* + %loaded2 = load i64, i64 addrspace(10)* %casted2 + ret void +} + attributes #0 = { argmemonly norecurse nounwind readonly } attributes #1 = { inaccessiblememonly norecurse nounwind } !0 = !{!"jtbaa"} !1 = !{!2, !2, i64 0} -!2 = !{!"jtbaa_immut", !0, i64 0} +!2 = !{!"jtbaa_value", !0, i64 0} !3 = !{!"jtbaa_const", !0, i64 0} !4 = !{!3, !3, i64 0, i64 1} +!5 = !{!6, !6, i64 0} +!6 = !{!"jtbaa_memorylen", !2, i64 0} + +; The memory-region '!alias.scope' domain (`jl_regions_t` in codegen.cpp). +!7 = !{!8} +!8 = !{!"jnoalias_immutdata", !9} +!9 = !{!"jnoalias"} +!10 = !{!11} +!11 = !{!"jnoalias_memorybuf", !9} +!12 = !{!8, !11} +; Same scope name, but a domain that is not ours. +!15 = !{!16} +!16 = !{!"jnoalias_immutdata", !17} +!17 = !{!"not_jnoalias"} + +!18 = !{} + +!19 = !{!20} +!20 = !{!"jnoalias_mutconstdata", !9} + From 7485f7ad53e7a52bc834bbcbd2bfbcfcd8ea2810 Mon Sep 17 00:00:00 2001 From: Sam Schweigel Date: Fri, 18 Sep 2026 07:48:35 -0700 Subject: [PATCH 02/12] Declare _reinterpret up front to avoid a ton of invalidations (#62871) The binding invalidation code is really coarse-grained, so it's better to declare `_reinterpret` up front, in `essentials.jl`, rather than refer to an undefined function and define it later. Reduces the system image size by about 4 MiB (2.2%) and program text by 184 KiB (1.2%). --- base/essentials.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/base/essentials.jl b/base/essentials.jl index 6d8251d0d9370..25557850819bf 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -745,6 +745,11 @@ unsafe_convert(::Type{P}, x::Ptr) where {P<:Ptr} = convert(P, x) unsafe_convert(::Type{Ptr{UInt8}}, s::String) = ccall(:jl_string_ptr, Ptr{UInt8}, (Any,), s) unsafe_convert(::Type{Ptr{Int8}}, s::String) = ccall(:jl_string_ptr, Ptr{Int8}, (Any,), s) +# We don't add any _reinterpret methods until we include reinterpretarray.jl, +# but defining the function up front avoids a whole lot of invalidations when we +# do. +function _reinterpret end + """ reinterpret(::Type{Out}, x::In) From 22e4766fdef43e6b52949531596ea8d924b1b328 Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Fri, 18 Sep 2026 22:09:28 +0200 Subject: [PATCH 03/12] deps: pass `CMAKE_POLICY_VERSION_MINIMUM` when configuring ittapi (#63265) --- deps/ittapi.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/ittapi.mk b/deps/ittapi.mk index f27ae2d6f77d5..788f60c156b0e 100644 --- a/deps/ittapi.mk +++ b/deps/ittapi.mk @@ -5,7 +5,7 @@ ITTAPI_GIT_URL := https://github.com/intel/ittapi.git ITTAPI_TAR_URL = https://api.github.com/repos/intel/ittapi/tarball/$1 $(eval $(call git-external,ittapi,ITTAPI,CMakeLists.txt,,$(SRCCACHE))) -ITTAPI_OPTS := $(CMAKE_COMMON) -DCMAKE_BUILD_TYPE=Release -DITT_API_IPT_SUPPORT= -DITT_API_FORTRAN_SUPPORT=0 +ITTAPI_OPTS := $(CMAKE_COMMON) -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DCMAKE_BUILD_TYPE=Release -DITT_API_IPT_SUPPORT= -DITT_API_FORTRAN_SUPPORT=0 $(BUILDDIR)/$(ITTAPI_SRC_DIR)/build-configured: $(SRCCACHE)/$(ITTAPI_SRC_DIR)/source-extracted mkdir -p $(dir $@) From 377e4c1869f827140ad5491d123dfda878c50de6 Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Fri, 18 Sep 2026 22:10:00 +0200 Subject: [PATCH 04/12] deps: do not build a dependency in order to uninstall it (#63264) --- deps/tools/common.mk | 5 +++-- deps/tools/uninstallers.mk | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/deps/tools/common.mk b/deps/tools/common.mk index bddd95476843b..172ece82409d6 100644 --- a/deps/tools/common.mk +++ b/deps/tools/common.mk @@ -200,9 +200,10 @@ install-$(strip $1): $$(build_prefix)/manifest/$(strip $1) ifeq (exists, $$(shell [ -e $$(build_staging)/$2.tar ] && echo exists )) # clean depends on uninstall only if the staged file exists distclean-$(strip $1) clean-$(strip $1): uninstall-$(strip $1) -else +else ifeq (exists, $$(shell [ -e $$(build_prefix)/manifest/$(strip $1) ] && echo exists )) # uninstall depends on staging only if the staged file doesn't exist -# otherwise, uninstall doesn't actually want the file to be updated first +# but the dep is installed; otherwise, uninstall doesn't actually want +# the file to be updated first (or has nothing to uninstall) uninstall-$(strip $1): | $$(build_staging)/$2.tar endif diff --git a/deps/tools/uninstallers.mk b/deps/tools/uninstallers.mk index 0051786ed1d0a..a7157b16b76d5 100644 --- a/deps/tools/uninstallers.mk +++ b/deps/tools/uninstallers.mk @@ -9,7 +9,10 @@ ifeq (undefined,$$(flavor $$(word 2,$$(MANIFEST_$1)))) MANIFEST_$1 := $$(UNINSTALL_$1) endif UNINST_HOW_$1 := $$(word 2,$$(MANIFEST_$1)) -ifneq ($$(UNINST_HOW_$1),) +ifeq (,$$(wildcard $$(build_prefix)/manifest/$1)) +uninstall-$1: + @echo "skipping uninstall: $1 not installed" +else ifneq ($$(UNINST_HOW_$1),) UNINST_WHO_$1 := $$(firstword $$(MANIFEST_$1)) UNINST_WHERE_$1 := $$(wordlist 3,99,$$(MANIFEST_$1)) $$(eval $$(call $$(UNINST_HOW_$1),$1,$$(UNINST_WHO_$1),$$(UNINST_WHERE_$1))) From a0c011e53a598d22360a1859ee69471ed719acc4 Mon Sep 17 00:00:00 2001 From: Andy Dienes <51664769+adienes@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:10:19 -0400 Subject: [PATCH 05/12] fixup big rational hashing (#63266) --- base/rational.jl | 2 +- test/gmp.jl | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/base/rational.jl b/base/rational.jl index 18bbf606ddc7f..ac3571547c64a 100644 --- a/base/rational.jl +++ b/base/rational.jl @@ -685,7 +685,7 @@ function hash(x::Rational{<:BitInteger64}, h::UInt) end end h = hash_integer(pow, h) - h = hash_integer((pow > 0) ? (num << (pow % 64)) : num, h) + h = hash_integer((pow > 0) ? (num << (pow % 8)) : num, h) return h end diff --git a/test/gmp.jl b/test/gmp.jl index 0a8b253a92a81..711ca9eaf4ee1 100644 --- a/test/gmp.jl +++ b/test/gmp.jl @@ -815,7 +815,7 @@ t = Rational{BigInt}(0, 1) end @testset "hashing" begin - for i in 1:10:100 + for i in vcat(0, 1:10:100) for shift in vcat(0:8, 9:8:81) for sgn in (1, -1) bint = sgn * (big(11)^i << shift) @@ -825,6 +825,15 @@ end @invoke(hash(bint::Real, Base.HASH_SEED)) @test Base.hash_integer(bint, Base.HASH_SEED) == @invoke(Base.hash_integer(bint::Integer, Base.HASH_SEED)) + + brat = bint // 3 + for T in Base.BitInteger_types + typemin(T) <= bint <= typemax(T) || continue + rat = Rational{T}(brat) + for h in (zero(UInt), Base.HASH_SEED, typemax(UInt)) + @test hash(rat, h) == hash(brat, h) + end + end end end end From 4d76908384bc853a4faeab2172d669c4ba8ec4ef Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Fri, 18 Sep 2026 23:28:47 +0200 Subject: [PATCH 06/12] gc: never skip weak-processing pages in the quick sweep (#63234) --- src/gc-stock.c | 12 +++++++++--- test/cancellation.jl | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/gc-stock.c b/src/gc-stock.c index 355b8cb9477ad..ff9eb2a878173 100644 --- a/src/gc-stock.c +++ b/src/gc-stock.c @@ -1103,8 +1103,12 @@ static void gc_sweep_page(gc_page_profiler_serializer_t *s, jl_gc_pool_t *p, jl_ goto done; } // For quick sweep, we might be able to skip the page if the page doesn't - // have any young live cell before marking. - if (!current_sweep_full && !pg->has_young) { + // have any young live cell before marking. Weak-processing pages must + // still be scanned: dead promoted cancellation sources must be unlinked + // in the same collection that frees their young referents. Otherwise a + // cancellation walk can reach a dead source with dangling fields. + if (!current_sweep_full && !pg->has_young && + !jl_atomic_load_relaxed(&pg->has_weak_processing)) { assert(!prev_sweep_full || pg->prev_nold >= pg->nold); if (!prev_sweep_full || pg->prev_nold == pg->nold) { freedall = 0; @@ -1355,7 +1359,9 @@ static int gc_sweep_prescan(jl_ptls_t ptls, jl_gc_padded_page_stack_t *new_gc_al if (!pg->has_marked) { should_scan = 0; } - if (!current_sweep_full && !pg->has_young) { + // Keep the quick-sweep condition in sync with gc_sweep_page. + if (!current_sweep_full && !pg->has_young && + !jl_atomic_load_relaxed(&pg->has_weak_processing)) { assert(!prev_sweep_full || pg->prev_nold >= pg->nold); if (!prev_sweep_full || pg->prev_nold == pg->nold) { should_scan = 0; diff --git a/test/cancellation.jl b/test/cancellation.jl index 283e9bc0209b9..602f1fe74a6b1 100644 --- a/test/cancellation.jl +++ b/test/cancellation.jl @@ -73,6 +73,24 @@ end @test length(kids) == 2 # kept + escaped; the dead child was spliced out @test kept in kids && escaped_tok.source in kids + # Dead promoted children must be unlinked even if their pages have no young + # allocations, before a cancellation walk can reach their freed walk locks. + @noinline function make_promoted_children(root, n) + promoted = [CancellationTokenSource(CancellationToken(root)) for _ in 1:n] + GC.gc(false) # promote the children before allocating their walk locks + foreach(cancel!, promoted) + return nothing + end + for _ in 1:20 + root3 = CancellationTokenSource() + kept3 = CancellationTokenSource(CancellationToken(root3)) + make_promoted_children(root3, 200) + GC.gc(false) + @test cancel!(root3) + # MMTk may retain promoted children until a full collection. + @test live_children(root3) == [kept3] skip=!Base.USING_STOCK_GC + end + # linked sources: a source with several parents is cancelled by any of # them (the graph is a DAG, not just a tree) la = CancellationTokenSource() From 035f7c9c2348982153d39d3a552dfe0d5c7b1aae Mon Sep 17 00:00:00 2001 From: Raye Kimmerer Date: Fri, 18 Sep 2026 16:21:47 -0400 Subject: [PATCH 07/12] doc: split latex symbols from emojis In lieu of increasing page limit sizes split latex symbols from emojis. Done to fix #60263 Assisted-by: Claude Code (Opus 5) --- doc/make.jl | 93 +++++++++++++++++++++++++++++++++ doc/src/manual/emoji-input.md | 23 ++++++++ doc/src/manual/unicode-input.md | 79 ++-------------------------- stdlib/REPL/docs/src/index.md | 3 +- 4 files changed, 123 insertions(+), 75 deletions(-) create mode 100644 doc/src/manual/emoji-input.md diff --git a/doc/make.jl b/doc/make.jl index f6f397350f5a0..65d0687cda48f 100644 --- a/doc/make.jl +++ b/doc/make.jl @@ -239,6 +239,98 @@ function generate_agent_skill_docs() end AgentSkillDocs = generate_agent_skill_docs() +# Shared machinery for the generated tab-completion tables in +# `manual/unicode-input.md` and `manual/emoji-input.md`. The LaTeX and emoji +# tables are each large enough that rendering both on one page takes the +# generated HTML past Documenter's `size_threshold`, so they live on separate +# pages and both call into this module. +module UnicodeTables + +import Markdown + +const NBSP = '\u00A0' + +# Invert the completion tables: map each completed string to every tab +# completion sequence that produces it. +function tab_completions(symbols...) + completions = Dict{String, Vector{String}}() + for each in symbols, (k, v) in each + completions[v] = push!(get!(completions, v, String[]), k) + end + return completions +end + +# `UnicodeData.txt` is downloaded into the build root by `doc/Makefile`, which +# is not the directory this file lives in for out-of-tree builds. +unicode_data_file() = joinpath(Main.buildrootdoc, "UnicodeData.txt") + +# Parsed once and shared by every page that renders a table. +const UNICODE_NAMES = Dict{UInt32, String}() + +function unicode_data() + isempty(UNICODE_NAMES) || return UNICODE_NAMES + open(unicode_data_file()) do unidata + for line in readlines(unidata) + id, name, desc = split(line, ";")[[1, 2, 11]] + codepoint = parse(UInt32, "0x$id") + UNICODE_NAMES[codepoint] = titlecase(lowercase( + name == "" ? desc : desc == "" ? name : "$name / $desc")) + end + end + return UNICODE_NAMES +end + +# Surround combining characters with no-break spaces (i.e '\u00A0'). Follows the same format +# for how unicode is displayed on the unicode.org website: +# https://util.unicode.org/UnicodeJsps/character.jsp?a=0300 +function fix_combining_chars(char) + cat = Base.Unicode.category_code(char) + return cat == 6 || cat == 8 ? "$NBSP$char$NBSP" : "$char" +end + +function table_entries(completions, unicode_dict) + entries = Any[Any[ + ["Code point(s)"], + ["Character(s)"], + ["Tab completion sequence(s)"], + ["Unicode name(s)"], + ]] + for (chars, inputs) in sort!(collect(completions), by = first) + code_points, unicode_names, characters = String[], String[], String[] + for char in chars + push!(code_points, "U+$(uppercase(string(UInt32(char), base = 16, pad = 5)))") + push!(unicode_names, get(unicode_dict, UInt32(char), "(No Unicode name)")) + push!(characters, isempty(characters) ? fix_combining_chars(char) : "$char") + end + inputs_md = [] + for (i, input) in enumerate(inputs) + i > 1 && push!(inputs_md, ", ") + push!(inputs_md, Markdown.Code("", input)) + end + push!(entries, [ + [join(code_points, " + ")], + [join(characters)], + inputs_md, + [join(unicode_names, " + ")], + ]) + end + table = Markdown.Table(entries, [:l, :c, :l, :l]) + # We also need to wrap the Table in a Markdown.MD "document" + return Markdown.MD([table]) +end + +""" + symbol_table(symbols...) + +Render the tab completions in `symbols` (dictionaries mapping a completion +sequence to the string it expands to, such as `REPL.REPLCompletions.latex_symbols`) +as a Markdown table, annotated with the code points and Unicode names of each +completed character. +""" +symbol_table(symbols...) = table_entries(tab_completions(symbols...), unicode_data()) + +end # module UnicodeTables + Manual = [ "manual/getting-started.md", "manual/installation.md", @@ -280,6 +372,7 @@ Manual = [ "manual/faq.md", "manual/noteworthy-differences.md", "manual/unicode-input.md", + "manual/emoji-input.md", "manual/command-line-interface.md", "manual/worldage.md", ] diff --git a/doc/src/manual/emoji-input.md b/doc/src/manual/emoji-input.md new file mode 100644 index 0000000000000..bb8e453cc3e7c --- /dev/null +++ b/doc/src/manual/emoji-input.md @@ -0,0 +1,23 @@ +# Emoji Input + +The following table lists the emoji that can be entered via tab completion of +their `\:name:` abbreviations in the Julia REPL (and in various other +editing environments). You can also get information on how to type an emoji by +entering it in the REPL help, i.e. by typing `?` and then entering the emoji in +the REPL (e.g., by copy-paste from somewhere you saw the emoji). + +For the LaTeX-like abbreviations that complete to mathematical symbols and +other non-emoji characters, see [Unicode Input](@ref). + +!!! warning + + This table may appear to contain missing characters in the second column, or even + show characters that are inconsistent with the characters as they are rendered in + the Julia REPL. In these cases, users are strongly advised to check their choice + of fonts in their browser and REPL environment, as there are known issues with + glyphs in many fonts. + +```@eval +import REPL +Main.UnicodeTables.symbol_table(REPL.REPLCompletions.emoji_symbols) +``` diff --git a/doc/src/manual/unicode-input.md b/doc/src/manual/unicode-input.md index eba970c051f1e..6e4b1430e5da2 100644 --- a/doc/src/manual/unicode-input.md +++ b/doc/src/manual/unicode-input.md @@ -7,6 +7,9 @@ type a symbol by entering it in the REPL help, i.e. by typing `?` and then entering the symbol in the REPL (e.g., by copy-paste from somewhere you saw the symbol). +Emoji are completed from their own set of abbreviations and are listed +separately in [Emoji Input](@ref). + !!! warning This table may appear to contain missing characters in the second column, or even @@ -16,78 +19,6 @@ the symbol). glyphs in many fonts. ```@eval -# -# Generate a table containing all LaTeX and Emoji tab completions available in the REPL. -# -import REPL, Markdown -const NBSP = '\u00A0' - -function tab_completions(symbols...) - completions = Dict{String, Vector{String}}() - for each in symbols, (k, v) in each - completions[v] = push!(get!(completions, v, String[]), k) - end - return completions -end - -function unicode_data() - file = normpath(@__DIR__, "..", "..", "..", "..", "..", "doc", "UnicodeData.txt") - names = Dict{UInt32, String}() - open(file) do unidata - for line in readlines(unidata) - id, name, desc = split(line, ";")[[1, 2, 11]] - codepoint = parse(UInt32, "0x$id") - names[codepoint] = titlecase(lowercase( - name == "" ? desc : desc == "" ? name : "$name / $desc")) - end - end - return names -end - -# Surround combining characters with no-break spaces (i.e '\u00A0'). Follows the same format -# for how unicode is displayed on the unicode.org website: -# https://util.unicode.org/UnicodeJsps/character.jsp?a=0300 -function fix_combining_chars(char) - cat = Base.Unicode.category_code(char) - return cat == 6 || cat == 8 ? "$NBSP$char$NBSP" : "$char" -end - -function table_entries(completions, unicode_dict) - entries = Any[Any[ - ["Code point(s)"], - ["Character(s)"], - ["Tab completion sequence(s)"], - ["Unicode name(s)"], - ]] - for (chars, inputs) in sort!(collect(completions), by = first) - code_points, unicode_names, characters = String[], String[], String[] - for char in chars - push!(code_points, "U+$(uppercase(string(UInt32(char), base = 16, pad = 5)))") - push!(unicode_names, get(unicode_dict, UInt32(char), "(No Unicode name)")) - push!(characters, isempty(characters) ? fix_combining_chars(char) : "$char") - end - inputs_md = [] - for (i, input) in enumerate(inputs) - i > 1 && push!(inputs_md, ", ") - push!(inputs_md, Markdown.Code("", input)) - end - push!(entries, [ - [join(code_points, " + ")], - [join(characters)], - inputs_md, - [join(unicode_names, " + ")], - ]) - end - table = Markdown.Table(entries, [:l, :c, :l, :l]) - # We also need to wrap the Table in a Markdown.MD "document" - return Markdown.MD([table]) -end - -table_entries( - tab_completions( - REPL.REPLCompletions.latex_symbols, - REPL.REPLCompletions.emoji_symbols - ), - unicode_data() -) +import REPL +Main.UnicodeTables.symbol_table(REPL.REPLCompletions.latex_symbols) ``` diff --git a/stdlib/REPL/docs/src/index.md b/stdlib/REPL/docs/src/index.md index 093b6559c6ac5..5dcdc3795b257 100644 --- a/stdlib/REPL/docs/src/index.md +++ b/stdlib/REPL/docs/src/index.md @@ -453,7 +453,8 @@ julia> α="\alpha[TAB]" # LaTeX completion also works in strings julia> α="α" ``` -A full list of tab-completions can be found in the [Unicode Input](@ref) section of the manual. +Full lists of tab-completions can be found in the [Unicode Input](@ref) and +[Emoji Input](@ref) sections of the manual. Completion of paths works for strings and julia's shell mode: From a261da0be47a8b359c42fe0edfe2d1703c87964a Mon Sep 17 00:00:00 2001 From: Cody Tapscott <84105208+topolarity@users.noreply.github.com> Date: Fri, 18 Sep 2026 20:05:54 -0400 Subject: [PATCH 08/12] gc: Make stock-GC memory copies safe under concurrent mutation (#63244) A mutable source Memory can receive young references after its generation state is checked, and a source scan can disagree with the values later copied. Neither can justify skipping the destination barrier. Copy first, then remember old destinations without inspecting the source. For copies containing at most four pointer fields, atomically scan the copied destination instead of remembering it unconditionally. There is no intervening safepoint, and concurrent replacement stores carry their own barriers. This bounds mutator scanning independently of destination size. Assisted-by: Codex (GPT-6) --- src/gc-wb-stock.h | 97 +++++++++++++++++--------------------- src/julia.h | 1 + test/gc/copyto.jl | 116 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 54 deletions(-) diff --git a/src/gc-wb-stock.h b/src/gc-wb-stock.h index 00fe8b0663b61..f007c41dbf0e6 100644 --- a/src/gc-wb-stock.h +++ b/src/gc-wb-stock.h @@ -60,74 +60,63 @@ STATIC_INLINE void jl_gc_multi_wb(const void *parent, const jl_value_t *ptr) JL_ jl_gc_queue_multiroot((jl_value_t*)parent, ptr, dt); } +// Maximum number of pointer fields to scan before remembering the owner. +#define JL_GC_COPY_SCAN_MAX_POINTERS 4 + STATIC_INLINE void jl_gc_genericmemory_copy_boxed(const jl_value_t *dest_owner, _Atomic(void*) *dest_p, - jl_genericmemory_t *src, _Atomic(void*) *src_p, - size_t n) JL_NOTSAFEPOINT + jl_genericmemory_t *src JL_UNUSED, _Atomic(void*) *src_p, + size_t n) JL_NOTSAFEPOINT { - if (__unlikely(jl_astaggedvalue(dest_owner)->bits.gc == 3 /* GC_OLD_MARKED */ )) { - if (__unlikely(jl_astaggedvalue(dest_owner)->bits.in_image == 1 /* GC_IN_IMAGE_NOT_REMSET */)) { - // GC_MARKED optimizations are invalid for generations >= 2 - jl_gc_queue_root(dest_owner); - } - else if (jl_astaggedvalue(jl_genericmemory_owner(src))->bits.gc != 3 /* GC_OLD_MARKED */) { - // check each value as it is stored, until the first young one queues the - // owner; the rest of the copy then needs no checks - size_t done = 0; - if (dest_p < src_p || dest_p > src_p + n) { - for (; done < n; done++) { // copy forwards - void *val = jl_atomic_load_relaxed(src_p + done); - jl_atomic_store_release(dest_p + done, val); - // `val` is young or old-unmarked (or dest is image and val is non-image) - if (val && !(jl_astaggedvalue(val)->bits.gc & 1 /* GC_MARKED */)) { - jl_gc_queue_root(dest_owner); - ++done; - break; - } - } - dest_p += done; - src_p += done; - } - else { - for (; done < n; done++) { // copy backwards - void *val = jl_atomic_load_relaxed(src_p + n - done - 1); - jl_atomic_store_release(dest_p + n - done - 1, val); - // `val` is young or old-unmarked (or dest is image and val is non-image) - if (val && !(jl_astaggedvalue(val)->bits.gc & 1 /* GC_MARKED */)) { - jl_gc_queue_root(dest_owner); - ++done; - break; - } - } + memmove_refs(dest_p, src_p, n); + if (n == 0 || __likely(jl_astaggedvalue(dest_owner)->bits.gc != 3 /* GC_OLD_MARKED */)) + return; // destination is young or remembered, or the copy is empty + if (n <= JL_GC_COPY_SCAN_MAX_POINTERS && + __likely(jl_astaggedvalue(dest_owner)->bits.in_image != 1 /* GC_IN_IMAGE_NOT_REMSET */)) { + // For small copies into old objects, scan what we just copied and see if all the + // elements were old to avoid adding `dest` to the remset, which saves a full scan + // of the destination memory at the next GC + for (size_t i = 0; i < n; i++) { + void *val = jl_atomic_load_relaxed(dest_p + i); + if (val && !(jl_astaggedvalue(val)->bits.gc & 1 /* GC_MARKED */)) { + jl_gc_queue_root(dest_owner); + return; } - n -= done; } + return; } - memmove_refs(dest_p, src_p, n); + jl_gc_queue_root(dest_owner); + return; } STATIC_INLINE void jl_gc_genericmemory_copy_ptr(const jl_value_t *owner, char *destdata, - jl_genericmemory_t *src, char *srcdata, + jl_genericmemory_t *src JL_UNUSED, char *srcdata, size_t n, jl_datatype_t *dt) JL_NOTSAFEPOINT { - size_t elsz = dt->layout->size; - if (__unlikely(jl_astaggedvalue(owner)->bits.gc == 3 /* GC_OLD_MARKED */)) { - if (__unlikely(jl_astaggedvalue(owner)->bits.in_image == 1 /* GC_IN_IMAGE_NOT_REMSET */)) { - // GC_MARKED optimizations are invalid for generations >= 2 - jl_gc_queue_root(owner); - } - else { - jl_value_t *src_owner = jl_genericmemory_owner(src); - if (jl_astaggedvalue(src_owner)->bits.gc != 3 /* GC_OLD_MARKED */) { - dt = (jl_datatype_t*)jl_tparam1(dt); - for (size_t done = 0; done < n; done++) { - char *s = srcdata + done * elsz; - if (*((jl_value_t**)s + dt->layout->first_ptr) != NULL) - jl_gc_queue_multiroot(owner, s, dt); + const jl_datatype_layout_t *ly = dt->layout; + size_t stride = ly->size / sizeof(void*); + _Atomic(void*) *dest_p = (_Atomic(void*)*)destdata; + memmove_refs(dest_p, (_Atomic(void*)*)srcdata, n * stride); + if (n == 0 || __likely(jl_astaggedvalue(owner)->bits.gc != 3 /* GC_OLD_MARKED */)) + return; // destination is young or remembered, or the copy is empty + if (n * ly->npointers <= JL_GC_COPY_SCAN_MAX_POINTERS && + __likely(jl_astaggedvalue(owner)->bits.in_image != 1 /* GC_IN_IMAGE_NOT_REMSET */)) { + // For small copies into old objects, scan what we just copied and see if all the + // elements were old to avoid adding `dest` to the remset, which saves a full scan + // of the destination memory at the next GC + for (size_t i = 0; i < n; i++) { + for (uint32_t j = 0; j < ly->npointers; j++) { + size_t offset = jl_ptr_offset(dt, j); + void *val = jl_atomic_load_relaxed(dest_p + i * stride + offset); + if (val && !(jl_astaggedvalue(val)->bits.gc & 1 /* GC_MARKED */)) { + jl_gc_queue_root(owner); + return; } } } + return; } - memmove_refs((_Atomic(void*)*)destdata, (_Atomic(void*)*)srcdata, n * elsz / sizeof(void*)); + jl_gc_queue_root(owner); + return; } STATIC_INLINE void jl_gc_genericmemory_clear(const jl_value_t *owner JL_UNUSED, diff --git a/src/julia.h b/src/julia.h index c72dbe97a0f48..89176b96d6f98 100644 --- a/src/julia.h +++ b/src/julia.h @@ -1385,6 +1385,7 @@ JL_DLLEXPORT JL_CONST_FUNC jl_gcframe_t **(jl_get_pgcstack)(void) JL_GLOBALLY_RO #define jl_current_task (container_of(jl_get_pgcstack(), jl_task_t, gcstack)) STATIC_INLINE jl_value_t *jl_genericmemory_owner(jl_genericmemory_t *m JL_PROPAGATES_ROOT) JL_NOTSAFEPOINT; +static inline uint32_t jl_ptr_offset(jl_datatype_t *st, int i) JL_NOTSAFEPOINT; // this is a version of memcpy that preserves atomic memory ordering // which makes it safe to use for objects that can contain memory references diff --git a/test/gc/copyto.jl b/test/gc/copyto.jl index 042de4072cbc1..53d5db1dc3fde 100644 --- a/test/gc/copyto.jl +++ b/test/gc/copyto.jl @@ -10,3 +10,119 @@ copyto!(dest, src) for (x, y) in zip(dest, src) @assert x === y end + +# Young references copied into old memories must survive after the source is gone, +# for small and large spans, inline pointer fields, and wide pointer descriptors. +struct CopyInline + first::Any + payload::Int + last::Any +end + +struct CopyWideInline + padding::NTuple{256,UInt} + value::Any +end + +@noinline function copy_young_refs!(dest, make_element, n) + src = [make_element(i) for i in 1:n] + copyto!(dest, 3, src, 1, n) + return nothing +end + +for (make_element, read_element) in ( + (Ref{Int}, x -> x[]), + (i -> CopyInline(nothing, i, Ref(i)), x -> (x.first, x.payload, x.last[])), + (i -> CopyWideInline(ntuple(_ -> UInt(0), 256), Ref(i)), x -> x.value[]), +) + for n in (1, 2, 3, 4, 5, 65), len in (n + 4, 4n + 4) + local dest = fill(make_element(0), len) + for _ in 1:3 + GC.gc(true) + GC.gc(false) + end + copy_young_refs!(dest, make_element, n) + GC.gc(false) + GC.gc(false) + for i in 1:n + @assert read_element(dest[i + 2]) == read_element(make_element(i)) + end + @assert read_element(dest[1]) == read_element(make_element(0)) + @assert read_element(dest[end]) == read_element(make_element(0)) + end +end + +struct CopyUndef + value::Any + CopyUndef() = new() +end + +struct CopyPartial + first::CopyUndef + last::Any +end + +@noinline function copy_partial!(dest) + child = Ref(42) + src = [CopyPartial(CopyUndef(), child)] + weak = WeakRef(child) + copyto!(dest, src) + return weak +end + +let dest = fill(CopyPartial(CopyUndef(), nothing), 8) + for _ in 1:3 + GC.gc(true) + GC.gc(false) + end + weak = copy_partial!(dest) + GC.@preserve dest begin + GC.gc(false) + GC.gc(false) + @assert weak.value !== nothing + @assert weak.value[] == 42 + end +end + +# A concurrent store into an old source must not let a copied young reference escape the barrier. +@noinline copy_race_ready(flag) = flag[] +@noinline copy_race_started(dest) = dest[1] === :src +@noinline copy_race_holds(dest, weak) = dest[end] === weak.value + +@noinline function copy_race_writer!(src, dest, ready) + child = Ref(42) + weak = WeakRef(child) + Threads.atomic_xchg!(ready, true) + while !copy_race_started(dest) end + src[end] = child + return weak +end + +function copy_race_trial(n) + src = fill!(Memory{Any}(undef, n), :src) + dest = fill!(Memory{Any}(undef, n), :dest) + GC.gc(true) + GC.gc(true) + ready = Threads.Atomic{Bool}(false) + result = Ref{WeakRef}() + Threads.@threads :static for worker in 1:2 + if worker == 1 + while !copy_race_ready(ready) end + copyto!(dest, src) + else + result[] = copy_race_writer!(src, dest, ready) + end + end + weak = result[] + copy_race_holds(dest, weak) || return false + src[end] = nothing + GC.@preserve dest begin + GC.gc(false) + @assert weak.value !== nothing "concurrently copied child was collected" + end + return true +end + +if Threads.nthreads(:default) >= 2 + @assert any(_ -> copy_race_trial(1_000_000), 1:10) "copy race interleaving was not exercised" +end From 58e8bb8cb772cbcb030a9affb061e0e7cf0265f9 Mon Sep 17 00:00:00 2001 From: Nicholas Bauer Date: Fri, 18 Sep 2026 22:05:37 -0400 Subject: [PATCH 09/12] Hide kwcall frames in stacktraces again (#63192) * Handle CodeInstance linfo * Add test * comment on purpose of branch * Use existing helper method Co-authored-by: Andy Dienes <51664769+adienes@users.noreply.github.com> --------- Co-authored-by: Andy Dienes <51664769+adienes@users.noreply.github.com> --- base/errorshow.jl | 4 ++-- test/errorshow.jl | 50 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/base/errorshow.jl b/base/errorshow.jl index 927c79b5357e3..94397444ea3b5 100644 --- a/base/errorshow.jl +++ b/base/errorshow.jl @@ -1195,7 +1195,7 @@ end function _backtrace_remove_kwcall_frames!(trace) todelete = findall(trace) do (frame, _) - code = frame.linfo + code = StackTraces.frame_mi(frame) if code isa MethodInstance def = code.def if def isa Method && def.name !== :kwcall && def.sig <: Tuple{typeof(Core.kwcall),NamedTuple,Any,Vararg} @@ -1204,7 +1204,7 @@ function _backtrace_remove_kwcall_frames!(trace) # the argument list, since it has the right line number info) return true end - else + else # this branch may not be needed, from before current keyword argument handling frame.func === :kwcall && return true end return false diff --git a/test/errorshow.jl b/test/errorshow.jl index 0d29dd2d0384f..0192e5aa88207 100644 --- a/test/errorshow.jl +++ b/test/errorshow.jl @@ -1683,6 +1683,56 @@ let bt @test occursin("simplify_kwargs_type(pos::$Int; kws::@Kwargs{kw1::Float64, kw2::String})", bt_str) end +# the internal `Core.kwcall` sorter frame is hidden in favor of the demangled keyword method +@noinline kwsorter_outer(; s = 4) = kwsorter_inner(s) +@noinline kwsorter_inner(s) = error() +@testset "kwcall frames" begin + # the trace also runs through the keyword calls of whatever invoked this test, so + # identify only the sorter frame belonging to `kwsorter_outer` + function iskwsorterframe(frame, @nospecialize(f)) + code = frame.linfo isa Core.CodeInstance ? frame.linfo.def : frame.linfo + if code isa Core.MethodInstance + def = code.def + return def isa Method && def.name !== :kwcall && + def.sig <: Tuple{typeof(Core.kwcall),NamedTuple,typeof(f),Vararg} + end + return frame.func === :kwcall + end + bt = try + kwsorter_outer(s = 3) + nothing + catch + catch_backtrace() + end + @test bt !== nothing + st = stacktrace(bt) + # the raw trace contains the keyword sorter frame ... + @test count(frame -> iskwsorterframe(frame, kwsorter_outer), st) == 1 + # ... which `process_backtrace` drops + @test !any(((frame, _),) -> iskwsorterframe(frame, kwsorter_outer), Base.process_backtrace(st)) + # leaving the demangled keyword method as the only frame for `kwsorter_outer` + bt_str = sprint(Base.show_backtrace, bt) + @test occursin("kwsorter_outer(; s::$Int)", bt_str) + @test !occursin("kwcall", bt_str) + @test count(l -> occursin(r"\] kwsorter_outer\(", l), split(bt_str, '\n')) == 1 + + # Depending on the debug info available, the sorter frame carries a `MethodInstance`, a + # `CodeInstance`, or no code at all, so check every spelling rather than just the one + # this platform happens to produce. + kwframe = st[findfirst(frame -> iskwsorterframe(frame, kwsorter_outer), st)] + mi = kwframe.linfo isa Core.CodeInstance ? kwframe.linfo.def : kwframe.linfo + @test mi isa Core.MethodInstance + userframe = Base.StackTraces.StackFrame(:f, Symbol("user.jl"), 1) + kept(frame) = [f.func for (f, _) in Base.process_backtrace(Base.StackFrame[frame, userframe])] + @testset "linfo::$(nameof(typeof(linfo)))" for linfo in Any[mi, mi.cache] + # recognized by its signature, whichever way the code is attached + @test kept(Base.StackTraces.StackFrame(kwframe.func, kwframe.file, kwframe.line, + linfo, false, false, 0)) == [:f] + end + # with no code attached at all, the frame is recognized by name instead + @test kept(Base.StackTraces.StackFrame(:kwcall, Symbol("none"), 1)) == [:f] +end + # Test Base.print_with_compare in convert MethodErrors struct TypeCompareError{A,B} <: Exception end let e = @test_throws MethodError convert(TypeCompareError{Float64,1}, TypeCompareError{Float64,2}()) From 346d02acc25c3c4c7d56f2fa825a0af0b6b5cd79 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com> Date: Sat, 19 Sep 2026 11:06:18 +0900 Subject: [PATCH 10/12] reflection: Add `world` keyword argument to `Base.bodyfunction` (#63239) `Base.bodyfunction` resolved the keyword body function with `getfield(mod, name)`, which looks up the binding in the world age of the calling task. A long-lived task that was spawned before the method was defined therefore hit the backdated binding path, which prints the `access to binding in a world prior to its definition world` warning and throws `UndefVarError` under `--depwarn=error`. JETLS ran into this when its analysis worker tasks inspected methods that were defined after the workers had started. This change adds a `world::UInt=get_world_counter()` keyword argument and performs the binding lookup with `invoke_in_world`, consistently with other reflection utilities such as `hasmethod` and `kwarg_decl`. Existing positional calls keep working, and they now resolve the body function in the latest world regardless of the world age of the caller. Assisted-by: Claude Code (Fable 5.1) --- base/reflection.jl | 17 ++++++++++++----- test/reflection.jl | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/base/reflection.jl b/base/reflection.jl index c5a38b6e8095c..44eac1f74e6e1 100644 --- a/base/reflection.jl +++ b/base/reflection.jl @@ -1130,13 +1130,20 @@ function hasmethod(f, t, kwnames::Tuple{Vararg{Symbol}}; world::UInt=get_world_c end """ - fbody = bodyfunction(basemethod::Method) + fbody = bodyfunction(basemethod::Method; world::UInt=Base.get_world_counter()) Find the keyword "body function" (the function that contains the body of the method as written, called after all missing keyword-arguments have been assigned default values). `basemethod` is the method you obtain via [`which`](@ref) or [`methods`](@ref). + +The binding of the body function is looked up in the world age given by `world`, which +defaults to the current world counter. + +!!! compat "Julia 1.14" + The `world` keyword argument requires Julia 1.14 or later. Before Julia 1.14, + the binding was looked up in the world age of the calling task. """ -function bodyfunction(basemethod::Method) +function bodyfunction(basemethod::Method; world::UInt=get_world_counter()) fmod = parentmodule(basemethod) # The lowered code for `basemethod` should look like # %1 = mkw(kwvalues..., #self#, args...) @@ -1149,7 +1156,7 @@ function bodyfunction(basemethod::Method) fsym = callexpr.args[1] while true if isa(fsym, Symbol) - return getfield(fmod, fsym) + return invoke_in_world(world, getglobal, fmod, fsym) elseif isa(fsym, GlobalRef) if fsym.mod === Core && fsym.name === :_apply fsym = callexpr.args[2] @@ -1157,9 +1164,9 @@ function bodyfunction(basemethod::Method) fsym = callexpr.args[3] end if isa(fsym, Symbol) - return getfield(fmod, fsym)::Function + return invoke_in_world(world, getglobal, fmod, fsym)::Function elseif isa(fsym, GlobalRef) - return getfield(fsym.mod, fsym.name)::Function + return invoke_in_world(world, getglobal, fsym.mod, fsym.name)::Function elseif isa(fsym, Core.SSAValue) fsym = ast.code[fsym.id] else diff --git a/test/reflection.jl b/test/reflection.jl index 6a03268166f3f..db27aa17e4c77 100644 --- a/test/reflection.jl +++ b/test/reflection.jl @@ -1293,6 +1293,20 @@ end @test occursin("f2#", String(nameof(f))) end +module BodyFunctionWorld +const before = Base.get_world_counter() +f(x; a=1) = x + a +end + +@testset "bodyfunction world age" begin + m = only(methods(BodyFunctionWorld.f)) + # the body function is found even when the calling task runs in a world age + # that predates its definition + f = Base.invoke_in_world(BodyFunctionWorld.before, Base.bodyfunction, m) + @test occursin("f#", String(nameof(f))) + @test Base.bodyfunction(m; world=m.primary_world) === f +end + @testset "code_typed(; world)" begin mod = @eval module $(gensym()) end From 5b35656624fe039678dda7235b3842535779d78a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 22:30:20 +0000 Subject: [PATCH 11/12] joolia: Adapt upstream batch to zero-origin semantics Assisted-by: Codex (GPT-5.6 Luna) --- doc/make.jl | 4 ++-- test/docs.jl | 15 +++++++++++++++ test/gc/copyto.jl | 8 ++++---- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/doc/make.jl b/doc/make.jl index 65d0687cda48f..f762139a00e10 100644 --- a/doc/make.jl +++ b/doc/make.jl @@ -271,7 +271,7 @@ function unicode_data() isempty(UNICODE_NAMES) || return UNICODE_NAMES open(unicode_data_file()) do unidata for line in readlines(unidata) - id, name, desc = split(line, ";")[[1, 2, 11]] + id, name, desc = split(line, ";")[[0, 1, 10]] codepoint = parse(UInt32, "0x$id") UNICODE_NAMES[codepoint] = titlecase(lowercase( name == "" ? desc : desc == "" ? name : "$name / $desc")) @@ -304,7 +304,7 @@ function table_entries(completions, unicode_dict) end inputs_md = [] for (i, input) in enumerate(inputs) - i > 1 && push!(inputs_md, ", ") + i > 0 && push!(inputs_md, ", ") push!(inputs_md, Markdown.Code("", input)) end push!(entries, [ diff --git a/test/docs.jl b/test/docs.jl index bf579658ea16c..666c0b6e21db5 100644 --- a/test/docs.jl +++ b/test/docs.jl @@ -13,6 +13,21 @@ end using Markdown using REPL +# The generated Unicode/emoji documentation tables are built by doc/make.jl. +# Keep the two origin-sensitive operations here covered independently of the +# Documenter build: UnicodeData fields are selected from a zero-origin split, +# and enumerate's zero-origin counter separates subsequent completions. +let fields = vcat(["0041", "LATIN CAPITAL LETTER A"], fill("", 8), ["A"]) + @test fields[[0, 1, 10]] == ["0041", "LATIN CAPITAL LETTER A", "A"] +end +let rendered = Any[] + for (i, input) in enumerate(["\\alpha", "\\Alpha"]) + i > 0 && push!(rendered, ", ") + push!(rendered, Markdown.Code("", input)) + end + @test [x isa Markdown.Code ? x.code : x for x in rendered] == ["\\alpha", ", ", "\\Alpha"] +end + using REPL: @repl, repl_latex, _repl, accessible using InteractiveUtils: apropos diff --git a/test/gc/copyto.jl b/test/gc/copyto.jl index 53d5db1dc3fde..75e2b258f0545 100644 --- a/test/gc/copyto.jl +++ b/test/gc/copyto.jl @@ -26,7 +26,7 @@ end @noinline function copy_young_refs!(dest, make_element, n) src = [make_element(i) for i in 1:n] - copyto!(dest, 3, src, 1, n) + copyto!(dest, 2, src, 0, n) return nothing end @@ -45,9 +45,9 @@ for (make_element, read_element) in ( GC.gc(false) GC.gc(false) for i in 1:n - @assert read_element(dest[i + 2]) == read_element(make_element(i)) + @assert read_element(dest[i + 1]) == read_element(make_element(i)) end - @assert read_element(dest[1]) == read_element(make_element(0)) + @assert read_element(dest[0]) == read_element(make_element(0)) @assert read_element(dest[end]) == read_element(make_element(0)) end end @@ -86,7 +86,7 @@ end # A concurrent store into an old source must not let a copied young reference escape the barrier. @noinline copy_race_ready(flag) = flag[] -@noinline copy_race_started(dest) = dest[1] === :src +@noinline copy_race_started(dest) = dest[0] === :src @noinline copy_race_holds(dest, weak) = dest[end] === weak.value @noinline function copy_race_writer!(src, dest, ready) From 97138beb7236829c8ee5fc4a81274b553cc5d7f1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 22:30:20 +0000 Subject: [PATCH 12/12] upstream: Record reviewed sync batch Assisted-by: Codex (GPT-5.6 Luna) --- ...d02acc25c3c4c7d56f2fa825a0af0b6b5cd79.json | 263 ++++++++++++++++++ contrib/upstream-sync/state.json | 2 +- 2 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 contrib/upstream-sync/reports/346d02acc25c3c4c7d56f2fa825a0af0b6b5cd79.json diff --git a/contrib/upstream-sync/reports/346d02acc25c3c4c7d56f2fa825a0af0b6b5cd79.json b/contrib/upstream-sync/reports/346d02acc25c3c4c7d56f2fa825a0af0b6b5cd79.json new file mode 100644 index 0000000000000..2cdc7cf31747d --- /dev/null +++ b/contrib/upstream-sync/reports/346d02acc25c3c4c7d56f2fa825a0af0b6b5cd79.json @@ -0,0 +1,263 @@ +{ + "plan": { + "base_sha": "57d27b961063dae2323809b7ac71d10d5b7154de", + "previous_sha": "81093121d4e6cf469154d977e2ecd6b7488ea0de", + "fetched_sha": "74fb2bea8d751b14e3413a01c12979de46c250b2", + "target_sha": "346d02acc25c3c4c7d56f2fa825a0af0b6b5cd79", + "branch": "sync/julia-81093121d4e6-346d02acc25c", + "status": "ready", + "manual_reasons": [], + "commits": [ + { + "sha": "a414ee183a31f255b75f82f0dd110ac64e74ab9c", + "subject": "codegen: redesign aliasinfo again (#62904)", + "paths": [ + "Compiler/test/codegen.jl", + "src/cgutils.cpp", + "src/clangsa/GCCheckerReporting.cpp", + "src/codegen.cpp", + "src/intrinsics.cpp", + "src/llvm-codegen-shared.h", + "src/llvm-late-gc-lowering.cpp", + "test/llvmpasses/gcroots.ll", + "test/llvmpasses/refinements.ll" + ], + "changed_lines": 682, + "patch_bytes": 68927, + "binary": false + }, + { + "sha": "7485f7ad53e7a52bc834bbcbd2bfbcfcd8ea2810", + "subject": "Declare _reinterpret up front to avoid a ton of invalidations (#62871)", + "paths": [ + "base/essentials.jl" + ], + "changed_lines": 5, + "patch_bytes": 654, + "binary": false + }, + { + "sha": "22e4766fdef43e6b52949531596ea8d924b1b328", + "subject": "deps: pass `CMAKE_POLICY_VERSION_MINIMUM` when configuring ittapi (#63265)", + "paths": [ + "deps/ittapi.mk" + ], + "changed_lines": 2, + "patch_bytes": 712, + "binary": false + }, + { + "sha": "377e4c1869f827140ad5491d123dfda878c50de6", + "subject": "deps: do not build a dependency in order to uninstall it (#63264)", + "paths": [ + "deps/tools/common.mk", + "deps/tools/uninstallers.mk" + ], + "changed_lines": 10, + "patch_bytes": 1545, + "binary": false + }, + { + "sha": "a0c011e53a598d22360a1859ee69471ed719acc4", + "subject": "fixup big rational hashing (#63266)", + "paths": [ + "base/rational.jl", + "test/gmp.jl" + ], + "changed_lines": 13, + "patch_bytes": 1420, + "binary": false + }, + { + "sha": "4d76908384bc853a4faeab2172d669c4ba8ec4ef", + "subject": "gc: never skip weak-processing pages in the quick sweep (#63234)", + "paths": [ + "src/gc-stock.c", + "test/cancellation.jl" + ], + "changed_lines": 30, + "patch_bytes": 2933, + "binary": false + }, + { + "sha": "a261da0be47a8b359c42fe0edfe2d1703c87964a", + "subject": "gc: Make stock-GC memory copies safe under concurrent mutation (#63244)", + "paths": [ + "src/gc-wb-stock.h", + "src/julia.h", + "test/gc/copyto.jl" + ], + "changed_lines": 214, + "patch_bytes": 10530, + "binary": false + }, + { + "sha": "035f7c9c2348982153d39d3a552dfe0d5c7b1aae", + "subject": "doc: split latex symbols from emojis", + "paths": [ + "doc/make.jl", + "doc/src/manual/emoji-input.md", + "doc/src/manual/unicode-input.md", + "stdlib/REPL/docs/src/index.md" + ], + "changed_lines": 198, + "patch_bytes": 9179, + "binary": false + }, + { + "sha": "e177e01f81c8571b2e21e84964446704b7645942", + "subject": "Merge pull request #63273 from JuliaLang/rk/split-latex-emoji-docs", + "paths": [ + "doc/make.jl", + "doc/src/manual/emoji-input.md", + "doc/src/manual/unicode-input.md", + "stdlib/REPL/docs/src/index.md" + ], + "changed_lines": 198, + "patch_bytes": 9179, + "binary": false + }, + { + "sha": "58e8bb8cb772cbcb030a9affb061e0e7cf0265f9", + "subject": "Hide kwcall frames in stacktraces again (#63192)", + "paths": [ + "base/errorshow.jl", + "test/errorshow.jl" + ], + "changed_lines": 54, + "patch_bytes": 4019, + "binary": false + }, + { + "sha": "346d02acc25c3c4c7d56f2fa825a0af0b6b5cd79", + "subject": "reflection: Add `world` keyword argument to `Base.bodyfunction` (#63239)", + "paths": [ + "base/reflection.jl", + "test/reflection.jl" + ], + "changed_lines": 31, + "patch_bytes": 3132, + "binary": false + } + ], + "stdlib_updates": [], + "limits": { + "upstream": "https://github.com/JuliaLang/julia.git", + "branch": "master", + "integrated_sha": "81093121d4e6cf469154d977e2ecd6b7488ea0de", + "max_commits": 20, + "max_changed_lines": 2500, + "max_patch_bytes": 250000 + }, + "remaining_first_parent_commits": [ + "decc791652f4becc636683071b94ddce617b8163", + "a6714ba33042a5aefca3c71c018c901ab46cda19", + "d96818f42f442fd51d506dc900cb2054004df78c", + "e9d1f89e1c79a44c4870fdfe6684ebe617c6e24b", + "6a1694d848366c4e75da889c2a060ebe58ea5cef", + "d024567ba68b457d767953002b34fe3dcc30f844", + "bbb0b4bb07506fa210a2aeae9e5042e1df6d90a2", + "3d9f720a065f30df545b0de30ce65b18115b0cba", + "286cd1c7abff3b55be76a6f6eaad89fb2d573235", + "26d28a1e242ed7947b2c920e328c4e0a0be6fb30", + "74fb2bea8d751b14e3413a01c12979de46c250b2" + ] + }, + "review": { + "target_sha": "346d02acc25c3c4c7d56f2fa825a0af0b6b5cd79", + "decision": "propose", + "summary": "Every incoming commit was reviewed in a separate job against the full merged batch.", + "unresolved": [], + "commits": [ + { + "sha": "a414ee183a31f255b75f82f0dd110ac64e74ab9c", + "risk": "high", + "subsystems": "LLVM code generation and alias analysis in src/codegen.cpp; late-GC metadata recognition in src/llvm-codegen-shared.h and src/llvm-late-gc-lowering.cpp; memory lowering in src/cgutils.cpp; intrinsic lowering in src/intrinsics.cpp; Clang analyzer diagnostics in src/clangsa/GCCheckerReporting.cpp; tests in Compiler/test/codegen.jl and test/llvmpasses/gcroots.ll/refinements.ll.", + "indexing_implications": "Region values (0 and bit flags) are memory-region enum values, not collection positions. LLVM metadata operands (getOperand(0), getOperand(3)), TBAA offset 0, vector capacities, and pointer/field counts retain their native meanings. Late-GC Number(S,V) values are internal SSA identifiers. Existing zero-origin paths in cgutils.cpp (emit_memoryref_direct, emit_memoryref, emit_memoryref_offset), codegen.cpp field lowering, and intrinsics.cpp pointerref/pointerset were inspected and are not changed by this commit.", + "adaptations": "None. The merged code changes alias metadata and GC-rooting classification only. memorybuf_aliasinfo, memoryselbyte, ai_is_private, and best_field_aliasinfo classify storage regions rather than translating positions. No Julia storage-boundary +1/-1 conversion or compiler-ID renumbering is introduced or lost.", + "tests": "Recommended, not executed: make -C test/llvmpasses refinements.ll; make -C test/llvmpasses gcroots.ll; run the foo54166 codegen test in Compiler/test/codegen.jl; run make -C test compiler-foundation in both supported compilation modes; rebuild before execution. Run the c-static-analysis procedure for codegen, late-GC, and Clang-analyzer units. Include checks that memoryref position 0 reaches the first element and that rooted immutable/const regions remain distinct from mutable memorybuf regions. No builds or tests were run." + }, + { + "sha": "7485f7ad53e7a52bc834bbcbd2bfbcfcd8ea2810", + "risk": "low", + "subsystems": "Base bootstrap and scalar/array reinterpretation in `base/essentials.jl`, `base/Base.jl`, and `base/reinterpretarray.jl`.", + "indexing_implications": "No indexing or dimension values change. `_reinterpret` is a function binding, not a position, count, sentinel, or external API value. `reinterpret` calls it for non-primitive values, and `reinterpretarray.jl` later supplies the implementation; existing zero-origin axes and storage offsets remain intact.", + "adaptations": "None. The declaration is compatible with Joolia's staged include order and does not require origin conversion.", + "tests": "Recommended, not executed: rebuild and run `make -C test base-foundation` plus the relevant `reinterpretarray` target. Cover scalar fallback, zero-origin first/last array positions, zero-length arrays, and multichannel reinterpretation." + }, + { + "sha": "22e4766fdef43e6b52949531596ea8d924b1b328", + "risk": "low", + "subsystems": "deps/ittapi CMake configuration and external dependency source builds", + "indexing_implications": "Inspected deps/ittapi.mk, deps/tools/common.mk, deps/ittapi.version, deps/Makefile, and deps/llvm.mk. The new 3.5 literal is a CMake policy-version value, not a position, dimension, count, identifier, or sentinel. Build paths and Make targets are dependency-graph names.", + "adaptations": "None. The merged code matches upstream intent; zero-origin translation would incorrectly alter CMake behavior. No Julia indexing contract is involved.", + "tests": "Recommended, not executed: make -C deps USE_BINARYBUILDER=0 configure-ittapi; make -C deps USE_BINARYBUILDER=0 compile-ittapi. No Julia regression test is meaningful for this build-only change." + }, + { + "sha": "377e4c1869f827140ad5491d123dfda878c50de6", + "risk": "low", + "subsystems": "Dependency build and uninstall infrastructure in deps/tools/common.mk and deps/tools/uninstallers.mk.", + "indexing_implications": "No Julia indexing is involved. Make `word 2`, `wordlist 3,99`, and `firstword` are one-based Make field operations, not Julia positions. Filesystem checks are boolean presence states; absent manifests must not trigger staging or compilation.", + "adaptations": "None. The merged files match the upstream change and correctly avoid building an uninstalled dependency while preserving reconstruction of staging data for an installed dependency whose tar is missing.", + "tests": "Not executed per restrictions. Recommend isolated-prefix checks for uninstall with no manifest, uninstall with a manifest but missing staging tar, `make -C deps USE_BINARYBUILDER=0 compile-zlib`, cleanup/uninstall, and the full CI gate." + }, + { + "sha": "a0c011e53a598d22360a1859ee69471ed719acc4", + "risk": "low", + "subsystems": "Base rational hashing in base/rational.jl (`hash(::Rational{<:BitInteger64})`); GMP/BigInt hashing coverage in test/gmp.jl; generic real hashing in base/hashing.jl and zero-origin `UnsafeLimbView` handling in base/gmp.jl.", + "indexing_implications": "`pow % 8` selects a bit shift within a byte; `pow` is a binary exponent and 8 is a byte width, not a position or dimension. Test variables `i`, `shift`, and `sgn` are exponent/sign/value generators; zero adds an exponent case. `T` iterates types and `h` iterates hash seeds, so neither is an index. The producer/consumer path from rational normalization through BigInt byte hashing was inspected. Joolia's `UnsafeLimbView` already uses zero-origin indexing and limb offsets. No sentinel, axis, tuple position, dimension argument, or foreign index changes.", + "adaptations": "None. The merged `base/rational.jl` correction preserves upstream intent by using the low three exponent bits for the byte-aligned hash payload shift, matching the generic rational path in base/hashing.jl. The added test/gmp.jl cases compare equivalent `Rational{T}` and `Rational{BigInt}` hashes across supported integer widths and seeds. Existing Joolia BigInt storage adaptations in base/gmp.jl are compatible, so changing these expressions or test values would be incorrect.", + "tests": "Recommended on a built Joolia artifact: `JULIA_TEST_FAILFAST=1 ./usr/bin/joolia --startup-file=no --check-bounds=yes test/runtests.jl gmp`, covering zero, positive and negative numerators, byte-boundary shifts, supported `BitInteger` widths, and all three hash seeds. Then run `python3 contrib/ci/run.py --jobs 4`. No tests were executed in this review job." + }, + { + "sha": "4d76908384bc853a4faeab2172d669c4ba8ec4ef", + "risk": "high", + "subsystems": "Stock GC page sweeping and cancellation-token weak child-list cleanup.", + "indexing_implications": "`has_weak_processing` and `has_young` are boolean flags; `nold`, `prev_nold`, `nfree`, `nparents`, and loop variables are counts or traversal counters, not collection positions. The existing zero-based `weak_processing_objs` traversal is unchanged. The guards in `gc_sweep_page` and `gc_sweep_prescan` preserve scanning whenever weak-processing work exists.", + "adaptations": "None. Joolia's existing wholesale-dead-page guard already preserves flagged pages, and the merged upstream change correctly extends that protection to both quick-sweep paths. The test ranges `1:n` and `1:20` represent repetition counts, not positions, so they must remain unchanged.", + "tests": "Recommended, not executed: rebuild and run `make -C src analyze-gc-stock -j8 --output-sync`; run the focused cancellation test with `JULIA_TEST_FAILFAST=1` (for example `make test-revise-cancellation` where available); then run `python3 contrib/ci/run.py --jobs 4`. Exercise repeated minor collections with 200 promoted linked children and verify cancellation completes without dangling walk-lock access; confirm the MMTk skip remains intentional." + }, + { + "sha": "a261da0be47a8b359c42fe0edfe2d1703c87964a", + "risk": "high", + "subsystems": "Stock GC write barriers and concurrent generic-memory copying in src/gc-wb-stock.h; runtime declarations in src/julia.h; GC regression coverage in test/gc/copyto.jl.", + "indexing_implications": "In src/gc-wb-stock.h, n is an element count, stride is a pointer-slot count, and j is a zero-based pointer-layout ordinal passed to src/julia.h:jl_ptr_offset. The test\u2019s explicit copyto! starts and direct reads are collection positions: destination 3/source 1 become 2/0, reads i+2/1 become i+1, and first sentinel 1 becomes 0. Loop bounds and worker labels remain counts or values. end remains the last valid position.", + "adaptations": "Changed test/gc/copyto.jl to use copyto!(dest, 2, src, 0, n), read copied elements at dest[i + 1], read the first sentinel at dest[0], and detect the concurrent copy start at dest[0]. No runtime change was needed because native pointer metadata is already zero-based and counts/ABI behavior are preserved.", + "tests": "Recommended, not run: rebuild the runtime, run the corresponding GC copyto test with one and multiple threads, and exercise n = 1, 2, 3, 4, 5, 65; inline and wide pointer-bearing structs; partial initialization; and the concurrent Memory{Any} race. Verify position 0, copied span 2:(n+1), untouched endpoints, and child liveness after source clearing. Run applicable Clang static analysis and the full CI gate on a built Joolia artifact." + }, + { + "sha": "035f7c9c2348982153d39d3a552dfe0d5c7b1aae", + "risk": "low", + "subsystems": "Documentation generator, Unicode/emoji manuals, and REPL documentation.", + "indexing_implications": "In doc/make.jl, split-field positions changed from [1, 2, 11] to [0, 1, 10]. The enumerate separator guard changed from i > 1 to i > 0 because enumerate is zero-origin. Code points and formatting metadata remain values, not indices.", + "adaptations": "Applied both narrow origin adaptations in doc/make.jl; upstream\u2019s page split and documentation links are unchanged.", + "tests": "Recommended, not executed: build documentation and inspect both generated Unicode and emoji tables, including multiple aliases and combining characters. git diff --check and checkpoint JSON validation were executed." + }, + { + "sha": "e177e01f81c8571b2e21e84964446704b7645942", + "risk": "low", + "subsystems": "Documentation generation (doc/make.jl, UnicodeTables); manual Unicode/emoji pages; REPL documentation; docs tests.", + "indexing_implications": "split(line, \";\") yields an ordinary zero-origin collection, so UnicodeData fields formerly selected at 1, 2, and 11 are positions 0, 1, and 10. enumerate(inputs) yields a zero-origin counter; i is used only to determine whether a separator precedes an alias, so later aliases satisfy i > 0. Code points, Unicode category codes, alignment symbols, paths, and list contents are values or metadata, not positions.", + "adaptations": "Changed doc/make.jl UnicodeData selection from [[1, 2, 11]] to [[0, 1, 10]], and completion separator logic from i > 1 to i > 0. Added test/docs.jl checks for field extraction and rendering multiple aliases. No adaptation was needed in the Markdown or REPL prose files.", + "tests": "Recommended: run the docs test target containing test/docs.jl; build documentation with the repository docs target and inspect both Unicode Input and Emoji Input generated tables, including entries with multiple aliases; run the REPL documentation suite in the standard CI gate. Not executed here because this review job forbids builds and tests." + }, + { + "sha": "58e8bb8cb772cbcb030a9affb061e0e7cf0265f9", + "risk": "medium", + "subsystems": "Base stack-trace filtering in base/errorshow.jl::_backtrace_remove_kwcall_frames!, Base.StackTraces.frame_mi, and test/errorshow.jl.", + "indexing_implications": "The change normalizes StackFrame metadata by unwrapping CodeInstance.def; it does not alter collection positions. The test's st[findfirst(...)] correctly uses Joolia's zero-origin result. Displayed frame labels remain human ordinals.", + "adaptations": "None. The merged implementation already uses the compatible frame_mi helper, and the imported regression test's indexing is consistent with Joolia's zero-origin contract. No source or test edit is justified.", + "tests": "Recommended, not run: rebuild Base, run make test-revise-errorshow, exercise MethodInstance/CodeInstance/no-linfo StackFrames and kwsorter_outer(; s=3), then run python3 contrib/ci/run.py --jobs 4." + }, + { + "sha": "346d02acc25c3c4c7d56f2fa825a0af0b6b5cd79", + "risk": "medium", + "subsystems": "Base reflection (`base/reflection.jl`, `bodyfunction`), Core world-age/global lookup, reflection tests (`test/reflection.jl`).", + "indexing_implications": "Existing Joolia conversions in `bodyfunction` are correct: `callexpr.args[0]` and related arguments are zero-origin Expr positions; `ast.code[end-1]` selects the penultimate statement; `Core.SSAValue.id` remains an internal one-origin identifier and is translated at storage access with `ast.code[fsym.id-1]`. The new `world` and `m.primary_world` values are world versions, not indices. Surrounding `method_argnames` handling was also inspected.", + "adaptations": "None. The commit only adds a typed `world` keyword and replaces world-insensitive `getfield` calls with `invoke_in_world(world, getglobal, ...)`. Additional index conversion would corrupt the existing AST/SSA boundary.", + "tests": "Recommended, not executed: `JULIA_TEST_FAILFAST=1 make test-revise-reflection`, relevant Base foundation checks, and the full CI gate. Exercise the `BodyFunctionWorld` regression with a pre-definition world and `world=m.primary_world`; verify Symbol and GlobalRef lookup paths and existing zero-origin AST/SSA behavior in `test/reflection.jl`." + } + ] + }, + "integrated": true +} diff --git a/contrib/upstream-sync/state.json b/contrib/upstream-sync/state.json index fbbb533faf8bb..22348abb3adb5 100644 --- a/contrib/upstream-sync/state.json +++ b/contrib/upstream-sync/state.json @@ -1,7 +1,7 @@ { "upstream": "https://github.com/JuliaLang/julia.git", "branch": "master", - "integrated_sha": "81093121d4e6cf469154d977e2ecd6b7488ea0de", + "integrated_sha": "346d02acc25c3c4c7d56f2fa825a0af0b6b5cd79", "max_commits": 20, "max_changed_lines": 2500, "max_patch_bytes": 250000