Skip to content

inference: allow PartialStruct to represent strict undef field#57541

Merged
giordano merged 1 commit intomasterfrom
avi/strict-undef-fields
Mar 2, 2025
Merged

inference: allow PartialStruct to represent strict undef field#57541
giordano merged 1 commit intomasterfrom
avi/strict-undef-fields

Conversation

@aviatesk
Copy link
Copy Markdown
Member

@aviatesk aviatesk commented Feb 26, 2025

This change allows PartialStruct to represent structs with strictly
uninitialized fields.

Now the previous undef::BitVector field is changed to
undefs::Vector{Union{Nothing,Bool}} to encode defined-ness information
of each field.

Also, this lets us fix the length of typ::PartialStruct's fields to
always match the number of fields in typ.typ. Instead of the current
design where the length of fields changes depending on the number of
initialized fields, it seems simpler to have PartialStructs
representing the same typ always have the same fields length.
So, I've included that refactoring as well.

  • fixes the newly detected error in Oscar.jl

@serenity4
Copy link
Copy Markdown
Member

The initial choice to make length(fields) <= length(fieldcount(typ)) was to improve performance (particularly so for large structs, but small structs also benefit from it). However, if benchmarks/build times show no noticeable regression, then the consistency is nice to have 👍

fldcnt = fieldcount_noerror(objt)::Int
fields = Any[fieldtype(objt0, i) for i = 1:fldcnt]
if fields[fldidx] === Union{}
return nothing # refine this field
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand the comment here, doesn't refining mean returning a PartialStruct? (though it makes sense to return nothing as a Union{} field type already implies undef[fldidx] === true)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR wasn't finished yesterday, so the logic might have been messed up. Undef the refined definition of PartialStruct, fields[fldidx] === Union{} is a special case, meaning that field is guaranteed not to be initialized, so we need to bail out here.

@aviatesk aviatesk force-pushed the avi/strict-undef-fields branch from c8dcd2c to 1397a8b Compare February 27, 2025 08:36
@aviatesk aviatesk changed the title wip: inference: allow PartialStruct to have Union{} field to indicate strict undef inference: allow PartialStruct to have Union{} field to indicate strict undef Feb 27, 2025
struct PartialStruct
typ
undef::BitVector # represents whether a given field may be undefined
undefs::Vector{Union{Nothing,Bool}} # represents whether a given field may be undefined
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed undef to undefs to avoid naming collision with the undef initializer.

Comment on lines -352 to -387
function refines_definedness_information(pstruct::PartialStruct)
nflds = length(pstruct.undef)
something(findfirst(pstruct.undef), nflds + 1) - 1 > datatype_min_ninitialized(pstruct.typ)
end

function define_field(pstruct::PartialStruct, fi::Int)
if !is_field_maybe_undef(pstruct, fi)
# no new information to be gained
return nothing
end

new = expand_partialstruct(pstruct, fi)
if new === nothing
new = PartialStruct(fallback_lattice, pstruct.typ, copy(pstruct.undef), copy(pstruct.fields))
end
new.undef[fi] = false
return new
end

function expand_partialstruct(pstruct::PartialStruct, until::Int)
n = length(pstruct.undef)
until ≤ n && return nothing

undef = partialstruct_init_undef(pstruct.typ, until; all_defined = false)
for i in 1:n
undef[i] &= pstruct.undef[i]
end
nf = length(pstruct.fields)
typ = pstruct.typ
fields = Any[i ≤ nf ? pstruct.fields[i] : fieldtype(typ, i) for i in 1:until]
return PartialStruct(fallback_lattice, typ, undef, fields)
end
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've inlined some logic implemented here because it is now only used once in other files.

@aviatesk aviatesk force-pushed the avi/strict-undef-fields branch from 1397a8b to b00838b Compare February 27, 2025 09:31
@aviatesk
Copy link
Copy Markdown
Member Author

@nanosoldier runbenchmarks("inference", vs=":master")

@nanosoldier
Copy link
Copy Markdown
Collaborator

Your benchmark job has completed - possible performance regressions were detected. A full report can be found here.


function Base.getproperty(pstruct::Core.PartialStruct, name::Symbol)
name === :undef && return getfield(pstruct, :undef)::BitVector
name === :undefs && return getfield(pstruct, :undefs)::Vector{Union{Nothing,Bool}}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NB: this method is not currently used from Compiler.jl. PR #57553 aims to fix that.

@nsajko nsajko added the compiler:inference Type inference label Feb 27, 2025
@aviatesk aviatesk force-pushed the avi/strict-undef-fields branch 2 times, most recently from e9ee6e4 to 1475330 Compare February 28, 2025 08:25
@aviatesk aviatesk added the merge me PR is reviewed. Merge when all tests are passing label Feb 28, 2025
@giordano
Copy link
Copy Markdown
Member

Is this broken on 32-bit platforms?

- `undefs[i] === false` indicates the corresponding element in `fields` is guaranteed to be defined
- `undefs[i] === true` indicates the corresponding element in `fields` is guaranteed to be undefined
If `field[i]` is of type `Union{}`, it means the `i`-th field is never be initialized and
thus never be defined. In this case, `undefs[i]` should always be `true`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for my understanding, was there any situation that we couldn't represent with the old encoding?

The docs seem to suggest undefs[i] === true <-> fields[i] === Union{} but maybe there's a corner-case where that's not true?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefs[i] === true -> fields[i] === Union{} is true, but the reverse isn't. Immutable structs with uninitialized pointer fields can have undefs[i] === true but at the same time fields[i] can be arbitrary types. We can probably make those fields[i] to Union{}, but I wanted to avoid encoding field defined-ness information into fields.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, that makes sense. Thanks for the details 👍

BTW, are you required to include anything that might come out of the type, or just what went into it? For example, if I make PartialStruct(Union{Foo,Bar}, undefs=[...], fields=[...]) where:

struct Foo
    a::Int
    b::Int
    Foo() = new()
end
struct Bar
    a::Any
    b::Any
    Bar() = new()
end

with rand(Bool) ? Foo() : Bar() (i.e. both fields undefined) - are the types in fields required to include Int, since that describes what we might read out of the isbits fields?

Or is it still correct to leave typ at Union{} in that case?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least for now, PartialStruct cannot represent objects that are inferred as Union types. The expression (x::PartialStruct).typ is actually either a DataType or a UnionAll.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UnionAll equivalent would be PartialStruct(Foo, ...) where:

struct Foo{T}
     a::T
     b::T
     Foo{T}() where T = new{T}()
 end

Foo{Int} is isbits and Foo{Any} is not

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In such cases, PartialStruct won't be created.
More precisely, in compilers that don't use MustAlias, PartialStruct.typ::DataType is always true. I remember a compiler with MustAlias sometimes creates PartialStruct with UnionAll at some point, but I think that only happens within JET. It might be safer to restrict PartialStruct.typ to DataType for now.

@aviatesk aviatesk changed the title inference: allow PartialStruct to have Union{} field to indicate strict undef inference: allow PartialStruct to represent strict undef field Feb 28, 2025
@aviatesk aviatesk force-pushed the avi/strict-undef-fields branch 2 times, most recently from 1545900 to 87fc6cf Compare February 28, 2025 21:11
This change allows `PartialStruct` to represent structs with strictly
uninitialized fields.

Now the previous `undef::BitVector` field is changed to
`undefs::Vector{Union{Nothing,Bool}}` to encode defined-ness information
of each field.

Also, this lets us fix the length of `typ::PartialStruct`'s `fields` to
always match the number of fields in `typ.typ`. Instead of the current
design where the length of `fields` changes depending on the number of
initialized fields, it seems simpler to have `PartialStruct`s
representing the same `typ` always have the same `fields` length.
So, I've included that refactoring as well.

- fixes the newly detected error in Oscar.jl

make `undefs::Vector{Union{Nothing,Bool}}`
@aviatesk aviatesk force-pushed the avi/strict-undef-fields branch from 87fc6cf to d10579b Compare March 1, 2025 16:55
@giordano giordano merged commit 5a2faa4 into master Mar 2, 2025
7 checks passed
@giordano giordano deleted the avi/strict-undef-fields branch March 2, 2025 11:28
@giordano giordano removed the merge me PR is reviewed. Merge when all tests are passing label Mar 2, 2025
for i = 1:length(fields)
@assert fields[i] !== Union{} # TODO remove me once we start to exploit strict undef-ness of fields
if fields[i] === Union{}
@assert undefs[i] === true "`Union{}` typed field should be strictly undefined"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Oscar.jl tests seem to be running into this assertion now:

 From worker 4:	Internal error: during type inference of
      From worker 4:	(::Type{Oscar.IntersectionTheory.AbstractVarietyMap{V1, V2} where V2<:Oscar.IntersectionTheory.AbstractVarietyT where V1<:Oscar.IntersectionTheory.AbstractVarietyT})(Oscar.IntersectionTheory.AbstractVariety, Oscar.IntersectionTheory.AbstractVariety, Array{Oscar.MPolyQuoRingElem{Oscar.MPolyDecRingElem{Nemo.QQFieldElem, Nemo.QQMPolyRingElem}}, 1}, AbstractAlgebra.Generic.FunctionalMap{Oscar.MPolyQuoRing{Oscar.MPolyDecRingElem{Nemo.QQFieldElem, Nemo.QQMPolyRingElem}}, Oscar.MPolyDecRing{Nemo.QQFieldElem, Nemo.QQMPolyRing}})
      From worker 4:	Encountered unexpected error in runtime:
      From worker 4:	AssertionError(msg="`Union{}` typed field should be strictly undefined")
      From worker 4:	PartialStruct at ./coreir.jl:62

https://github.com/oscar-system/Oscar.jl/actions/runs/13611555768/job/38059503585#step:11:677

serenity4 pushed a commit to serenity4/julia that referenced this pull request May 1, 2025
…iaLang#57541)

This change allows `PartialStruct` to represent structs with strictly
uninitialized fields.

Now the previous `undef::BitVector` field is changed to
`undefs::Vector{Union{Nothing,Bool}}` to encode defined-ness information
of each field.

Also, this lets us fix the length of `typ::PartialStruct`'s `fields` to
always match the number of fields in `typ.typ`. Instead of the current
design where the length of `fields` changes depending on the number of
initialized fields, it seems simpler to have `PartialStruct`s
representing the same `typ` always have the same `fields` length.
So, I've included that refactoring as well.

- fixes the newly detected error in Oscar.jl
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compiler:inference Type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants