From 87de46899d01963b0c7af3f731e5e1e4dfb67f8f Mon Sep 17 00:00:00 2001 From: PraneethMerugu Date: Tue, 8 Sep 2026 15:06:33 -0400 Subject: [PATCH 1/4] Support immutable named products in field publication --- docs/src/learn/localmath-domain-compiler.md | 6 + docs/src/learn/localmath-relations.md | 34 + spec/localmath.md | 6 + src/execution/mechanism_support.jl | 67 +- src/spatial_model.jl | 468 +++-- src/stage_model.jl | 1656 ++++++++++------- .../fixtures/product_publication_contracts.jl | 80 + test/metal/product_values.jl | 11 + test/metal/runtests.jl | 20 +- test/runtests.jl | 19 +- test/test_product_values.jl | 58 + test/test_spatial_model.jl | 22 +- test/test_stage_model.jl | 70 +- 13 files changed, 1561 insertions(+), 956 deletions(-) create mode 100644 test/fixtures/product_publication_contracts.jl create mode 100644 test/metal/product_values.jl create mode 100644 test/test_product_values.jl diff --git a/docs/src/learn/localmath-domain-compiler.md b/docs/src/learn/localmath-domain-compiler.md index 459ebe2..38a2ecd 100644 --- a/docs/src/learn/localmath-domain-compiler.md +++ b/docs/src/learn/localmath-domain-compiler.md @@ -74,3 +74,9 @@ The durable ownership split is: When lowering fails, retain the source origin from the domain operation. An unsupported footprint should be rejected cold with that provenance rather than captured in an opaque evaluator or guessed at runtime. + +Logical-value admission is owned by `spatial_model.jl`; evaluator capture uses +that same predicate in `stage_model.jl`. Physical record layout and leaf backend +operations are validated in `execution/mechanism_support.jl`, then consumed by +ordinary preparation and the KernelAbstractions executor. The product-value +publication tests exercise this chain without a separate packed-value executor. diff --git a/docs/src/learn/localmath-relations.md b/docs/src/learn/localmath-relations.md index c783cb3..9cd1b50 100644 --- a/docs/src/learn/localmath-relations.md +++ b/docs/src/learn/localmath-relations.md @@ -38,6 +38,40 @@ law = @localmath (i, j) ∈ interior(cells, 1) begin end ``` +## Structured field values + +A field's element type describes one logical value, independently of its spatial +shape. Immutable named products may combine admitted Boolean, integer, floating, +tuple, and bounded fixed-array values. Names are compile-time field labels; +runtime Symbols, pointers, references, and mutable arrays are not numeric leaves. + +```@example product_values +using LocalMath, KernelAbstractions + +@inline function advance_record(value) + return (active = !value.active, count = value.count + Int32(1), + polarity = (value.polarity[1] + 0.5f0, value.polarity[2])) +end + +initial = (active = false, count = Int32(2), polarity = (1.0f0, 2.0f0)) +cells = Space(3) +before = Field(cells, typeof(initial)) +after = Field(cells, typeof(initial)) +law = @localmath i ∈ cells begin + after[i] = advance_record(before[i]) +end +prepared = prepare(law, before => fill(initial, 3), + after => LocalMath.Allocate(undef); backend = KernelAbstractions.CPU()) +wait(execute!(prepared)) +@assert LocalMath.storage(prepared, after) == + fill((active = true, count = Int32(3), polarity = (1.5f0, 2.0f0)), 3) +nothing +``` + +Backend preparation still checks record layout and every leaf's load/store +support. Field admission does not imply that every reduction, atomic operation, +arbitrary record size, or numerical type is supported on every device. + ## Fixed mesh and graph topology Declare the mathematical direction and exact lane bound independently of the diff --git a/spec/localmath.md b/spec/localmath.md index 9c9c973..fc112a5 100644 --- a/spec/localmath.md +++ b/spec/localmath.md @@ -153,6 +153,12 @@ Record-valued Fields retain ordinary concrete isbits element types. Direct copies their component arrays to the selected backend while preserving shape, element type, and component structure. +Immutable named products use the same recursive storage-value admission as +tuples. Their names are compile-time labels, not runtime metadata values. +Preparation validates nested record offsets, sizes, alignment, and each leaf's +backend load/store support through the existing record-layout authority. +This does not extend an atomic or reduction guarantee beyond its own tested law. + Semantic descriptors have compact ordinary and `text/plain` displays. Descriptor presentation exposes scientific shape, element type, relation family, degree, storage requirement, capacity, optionality, boundary policy, diff --git a/src/execution/mechanism_support.jl b/src/execution/mechanism_support.jl index d5e2526..4c59d43 100644 --- a/src/execution/mechanism_support.jl +++ b/src/execution/mechanism_support.jl @@ -15,17 +15,21 @@ end # Stage mechanism. Keep its admission point beside the other centrally owned # mechanism capabilities rather than in a semantic family or planner. function _make_provider_lane(backend, storage) - throw(LocalMathValidationError( - "no centrally admitted provider-lane adapter exists for $(typeof(backend))" - )) + throw( + LocalMathValidationError( + "no centrally admitted provider-lane adapter exists for $(typeof(backend))" + ) + ) end function _central_make_provider_lane(backend, storage) signature = Tuple{typeof(backend), typeof(storage)} method = which(_make_provider_lane, signature) - method.module === (@__MODULE__) || throw(LocalMathValidationError( - "the provider-lane adapter is not centrally admitted" - )) + method.module === (@__MODULE__) || throw( + LocalMathValidationError( + "the provider-lane adapter is not centrally admitted" + ) + ) return invoke(_make_provider_lane, signature, backend, storage) end @@ -102,8 +106,10 @@ function _centrally_qualified_stage_record( backend, type::Type, operation::Symbol ) _storage_value_type(type) || return false - (_centrally_qualified_resolved_record(backend, type) || - _centrally_qualified_wide_resolved_record(backend, type)) || + ( + _centrally_qualified_resolved_record(backend, type) || + _centrally_qualified_wide_resolved_record(backend, type) + ) || return false return _record_leaf_capability(backend, type, operation) end @@ -130,10 +136,10 @@ function _centrally_qualified_resolved_record(backend, type::Type) offset += sizeof(field_type) packed && _centrally_qualified_value_capability( - backend, field_type, :load, :global, - ) && _centrally_qualified_value_capability( - backend, field_type, :store, :global, - ) + backend, field_type, :load, :global, + ) && _centrally_qualified_value_capability( + backend, field_type, :store, :global, + ) end return qualified && offset == sizeof(type) end @@ -162,24 +168,29 @@ function _centrally_qualified_wide_resolved_record(backend, type::Type) return false sizeof(type) <= _WIDE_RESOLVED_RECORD_MAX_BYTES && Base.datatype_alignment(type) <= - _WIDE_RESOLVED_RECORD_MAX_ALIGNMENT || return false + _WIDE_RESOLVED_RECORD_MAX_ALIGNMENT || return false previous_end = 0 for index in 1:fieldcount(type) field_type = fieldtype(type, index) - (isprimitivetype(field_type) || field_type <: Enum) || return false field_size = sizeof(field_type) field_size > 0 || return false offset = Base.fieldoffset(type, index) offset >= previous_end && offset + field_size <= sizeof(type) || return false - storage_type = _resolved_record_leaf_storage_type(field_type) - storage_type === Nothing && return false - _centrally_qualified_value_capability( - backend, storage_type, :load, :global, - ) && _centrally_qualified_value_capability( - backend, storage_type, :store, :global, - ) || return false + if isprimitivetype(field_type) || field_type <: Enum + storage_type = _resolved_record_leaf_storage_type(field_type) + storage_type === Nothing && return false + _centrally_qualified_value_capability( + backend, storage_type, :load, :global, + ) && _centrally_qualified_value_capability( + backend, storage_type, :store, :global, + ) || return false + else + _storage_value_type(field_type) && + _centrally_qualified_wide_resolved_record(backend, field_type) || + return false + end previous_end = offset + field_size end return true @@ -198,16 +209,18 @@ end function _storage_free_value(value) _storage_free_type(typeof(value)) || return false - return all(index -> _storage_free_value(getfield(value, index)), - 1:fieldcount(typeof(value))) + return all( + index -> _storage_free_value(getfield(value, index)), + 1:fieldcount(typeof(value)) + ) end _pointwise_effect_capability(backend, operation, signature) = false _centrally_qualified_pointwise_effects(backend, operation, signature) = _storage_free_value(operation) && _package_owned_capability_dispatch( - _pointwise_effect_capability, backend, operation, signature - ) && + _pointwise_effect_capability, backend, operation, signature +) && _pointwise_effect_capability(backend, operation, signature) _ordered_fold_effect_analysis(backend, transition, signature) = ( @@ -233,8 +246,8 @@ _ordering_effect_capability(backend, extractor, signature) = false _centrally_qualified_ordering_effects(backend, extractor, signature) = _storage_free_value(extractor) && _package_owned_capability_dispatch( - _ordering_effect_capability, backend, extractor, signature - ) && + _ordering_effect_capability, backend, extractor, signature +) && _ordering_effect_capability(backend, extractor, signature) function _device_copy(::KernelAbstractions.CPU, values) diff --git a/src/spatial_model.jl b/src/spatial_model.jl index 320922c..9522388 100644 --- a/src/spatial_model.jl +++ b/src/spatial_model.jl @@ -9,14 +9,14 @@ _new_semantic_identity() = UUIDs.uuid4() # One conservative storage-value predicate shared by semantic Fields and the # stage model. It deliberately excludes pointer/reference and metadata-rich # values even when Julia reports them as isbits. -_storage_value_type(::Type{T}) where {T<:Union{Number,Bool,Enum}} = +_storage_value_type(::Type{T}) where {T <: Union{Number, Bool, Enum}} = isconcretetype(T) && isbitstype(T) -@generated function _storage_value_type(::Type{T}) where {T<:Tuple} +@generated function _storage_value_type(::Type{T}) where {T <: Union{Tuple, NamedTuple}} qualified = isconcretetype(T) && isbitstype(T) && all(_storage_value_type, fieldtypes(T)) return qualified ? :(true) : :(false) end -function _storage_value_type(::Type{T}) where {T<:StaticArrays.StaticArray} +function _storage_value_type(::Type{T}) where {T <: StaticArrays.StaticArray} isconcretetype(T) && isbitstype(T) || return false dimensions = Tuple(StaticArrays.Size(T)) all(dimension -> 0 <= dimension <= 32, dimensions) || return false @@ -33,9 +33,11 @@ end @generated function _storage_value_type(::Type{T}) where {T} qualified = isconcretetype(T) && isbitstype(T) && - !(T <: Union{ - Symbol,UUIDs.UUID,Ptr,Ref,AbstractArray,NamedTuple,Val,Function, - }) && + !( + T <: Union{ + Symbol, UUIDs.UUID, Ptr, Ref, AbstractArray, NamedTuple, Val, Function, + } + ) && !(isdefined(Core, :LLVMPtr) && T <: Core.LLVMPtr) && isstructtype(T) && all(_storage_type_parameter, T.parameters) && @@ -47,19 +49,23 @@ function _checked_semantic_int(value::Integer, purpose::Symbol; positive = false converted = try Int(value) catch - throw(LocalMathValidationError( - "$purpose does not fit the host index type"; - stage = :construct, contract = purpose, - expected = :representable_integer, actual = value, - )) + throw( + LocalMathValidationError( + "$purpose does not fit the host index type"; + stage = :construct, contract = purpose, + expected = :representable_integer, actual = value, + ) + ) end lower = positive ? 1 : 0 - lower <= converted <= typemax(Int32) || throw(LocalMathValidationError( - "$purpose is outside the common Int32 execution-index ABI"; - stage = :construct, contract = purpose, - expected = positive ? (1:typemax(Int32)) : (0:typemax(Int32)), - actual = value, - )) + lower <= converted <= typemax(Int32) || throw( + LocalMathValidationError( + "$purpose is outside the common Int32 execution-index ABI"; + stage = :construct, contract = purpose, + expected = positive ? (1:typemax(Int32)) : (0:typemax(Int32)), + actual = value, + ) + ) return converted end @@ -69,35 +75,43 @@ function _checked_semantic_product(values, purpose::Symbol) product = try Base.checked_mul(product, Int(value)) catch - throw(LocalMathValidationError( - "$purpose overflows the common execution-index ABI"; - stage = :construct, contract = purpose, - expected = 0:typemax(Int32), actual = values, - )) + throw( + LocalMathValidationError( + "$purpose overflows the common execution-index ABI"; + stage = :construct, contract = purpose, + expected = 0:typemax(Int32), actual = values, + ) + ) end - product <= typemax(Int32) || throw(LocalMathValidationError( - "$purpose exceeds the common Int32 execution-index ABI"; - stage = :construct, contract = purpose, - expected = 0:typemax(Int32), actual = product, - )) + product <= typemax(Int32) || throw( + LocalMathValidationError( + "$purpose exceeds the common Int32 execution-index ABI"; + stage = :construct, contract = purpose, + expected = 0:typemax(Int32), actual = product, + ) + ) end return product end function _checked_schema_epoch(value::Integer) - value >= 0 || throw(LocalMathValidationError( - "a relation schema epoch must be nonnegative"; - stage = :construct, contract = :relation_schema_epoch, - expected = :uint64, actual = value, - )) + value >= 0 || throw( + LocalMathValidationError( + "a relation schema epoch must be nonnegative"; + stage = :construct, contract = :relation_schema_epoch, + expected = :uint64, actual = value, + ) + ) return try UInt64(value) catch - throw(LocalMathValidationError( - "a relation schema epoch does not fit UInt64"; - stage = :construct, contract = :relation_schema_epoch, - expected = :uint64, actual = value, - )) + throw( + LocalMathValidationError( + "a relation schema epoch does not fit UInt64"; + stage = :construct, contract = :relation_schema_epoch, + expected = :uint64, actual = value, + ) + ) end end @@ -109,34 +123,36 @@ struct _ProductSpaceStructure{F} end """A typed finite semantic index domain with stable value-level identity.""" -struct Space{K,N,S} +struct Space{K, N, S} id::UUIDs.UUID - extent::NTuple{N,Int} + extent::NTuple{N, Int} structure::S end function Space( - ::Type{K}, extent::Tuple{Vararg{Integer,N}}; + ::Type{K}, extent::Tuple{Vararg{Integer, N}}; id::UUIDs.UUID = _new_semantic_identity(), structure = _PlainSpaceStructure(), - ) where {K,N} - N > 0 || throw(LocalMathValidationError( - "a Space must have at least one dimension"; - stage = :construct, contract = :space_dimension, - expected = :positive, actual = N, - )) + ) where {K, N} + N > 0 || throw( + LocalMathValidationError( + "a Space must have at least one dimension"; + stage = :construct, contract = :space_dimension, + expected = :positive, actual = N, + ) + ) canonical = ntuple( axis -> _checked_semantic_int(extent[axis], :space_extent), Val(N) ) _checked_semantic_product(canonical, :space_cardinality) - return Space{K,N,typeof(structure)}(id, canonical, structure) + return Space{K, N, typeof(structure)}(id, canonical, structure) end Space(::Type{K}, extent::Integer; kwargs...) where {K} = Space(K, (extent,); kwargs...) """Construct an ordinary anonymous finite index space.""" -Space(extent::Tuple{Integer,Vararg{Integer}}; kwargs...) = +Space(extent::Tuple{Integer, Vararg{Integer}}; kwargs...) = Space(_IndexSpaceKind, extent; kwargs...) Space(::Tuple{}; kwargs...) = Space(_IndexSpaceKind, (); kwargs...) Space(extent::Integer; kwargs...) = Space(_IndexSpaceKind, extent; kwargs...) @@ -147,16 +163,20 @@ _IndexSpace(extent; id::UUIDs.UUID = _new_semantic_identity()) = function _ProductSpace( factors::Tuple; id::UUIDs.UUID = _new_semantic_identity() ) - isempty(factors) && throw(LocalMathValidationError( - "a product Space requires at least one factor"; - stage = :construct, contract = :product_space_factors, - expected = :nonempty_space_tuple, actual = factors, - )) - all(factor -> factor isa Space, factors) || throw(LocalMathValidationError( - "every product-space factor must be a Space"; - stage = :construct, contract = :product_space_factors, - expected = Space, actual = map(typeof, factors), - )) + isempty(factors) && throw( + LocalMathValidationError( + "a product Space requires at least one factor"; + stage = :construct, contract = :product_space_factors, + expected = :nonempty_space_tuple, actual = factors, + ) + ) + all(factor -> factor isa Space, factors) || throw( + LocalMathValidationError( + "every product-space factor must be a Space"; + stage = :construct, contract = :product_space_factors, + expected = Space, actual = map(typeof, factors), + ) + ) cardinality = _checked_semantic_product( map(length, factors), :product_space_cardinality ) @@ -173,7 +193,7 @@ Construct the Cartesian product of a nonempty tuple of finite spaces. Its cardinality is the product of the factor cardinalities, and factor order is the coordinate order used by `ProductRelation`. """ -Space(factors::Tuple{Space,Vararg{Space}}; kwargs...) = _ProductSpace(factors; kwargs...) +Space(factors::Tuple{Space, Vararg{Space}}; kwargs...) = _ProductSpace(factors; kwargs...) semantic_identity(space::Space) = space.id space_kind(::Space{K}) where {K} = K @@ -193,20 +213,22 @@ Base.hash(space::Space, seed::UInt) = hash( ) """Exact isbits element placement on a semantic `Space`; never storage.""" -struct Field{T,S<:Space} +struct Field{T, S <: Space} id::UUIDs.UUID space::S end function Field( space::S, ::Type{T}; id::UUIDs.UUID = _new_semantic_identity() - ) where {S<:Space,T} - _storage_value_type(T) || throw(LocalMathValidationError( - "a Field requires an admitted storage value type"; - stage = :construct, contract = :field_element_type, - expected = :numeric_bool_enum_tuple_or_static_array, actual = T, - )) - return Field{T,S}(id, space) + ) where {S <: Space, T} + _storage_value_type(T) || throw( + LocalMathValidationError( + "a Field requires an admitted storage value type"; + stage = :construct, contract = :field_element_type, + expected = :numeric_bool_enum_tuple_or_static_array, actual = T, + ) + ) + return Field{T, S}(id, space) end semantic_identity(field::Field) = field.id @@ -222,7 +244,7 @@ Base.hash(field::Field, seed::UInt) = hash( # mathematical payload types. UUIDs, extents, epochs, degrees and capacities # remain fields rather than specialization keys. struct _IdentityRelation end -struct _AffineRelation{O,R} +struct _AffineRelation{O, R} offsets::O origin::R end @@ -242,24 +264,24 @@ end struct StrictBoundary end """`PeriodicBoundary(axes)` wraps the selected Cartesian axes.""" struct PeriodicBoundary{D} - axes::NTuple{D,Bool} + axes::NTuple{D, Bool} end """`ExteriorBoundary()` represents out-of-domain lanes as absent samples.""" struct ExteriorBoundary end -struct MaskedBoundary{F,B} +struct MaskedBoundary{F, B} mask::F fallback::B end -struct GhostBoundary{D,S<:Space} - lower::NTuple{D,Int} - upper::NTuple{D,Int} +struct GhostBoundary{D, S <: Space} + lower::NTuple{D, Int} + upper::NTuple{D, Int} ghost_space::S end const _BoundaryPolicy = Union{ - StrictBoundary,PeriodicBoundary,ExteriorBoundary, - MaskedBoundary,GhostBoundary, + StrictBoundary, PeriodicBoundary, ExteriorBoundary, + MaskedBoundary, GhostBoundary, } -struct _BoundaryRelation{R,P} +struct _BoundaryRelation{R, P} base::R policy::P degree::Int @@ -275,12 +297,12 @@ struct _FieldIndexRelation{F} degree::Int optional::Bool end -struct _MaskedRelation{R,F} +struct _MaskedRelation{R, F} base::R mask::F degree::Int end -struct _SelectedRelation{R,I} +struct _SelectedRelation{R, I} base::R injection::I degree::Int @@ -297,7 +319,7 @@ struct _PackedRelation end """One storage-free semantic relation wrapper.""" -struct Relation{R,D<:Space,C<:Space} +struct Relation{R, D <: Space, C <: Space} id::UUIDs.UUID domain::D codomain::C @@ -329,7 +351,7 @@ degree_bound(relation::Relation{<:_AffineRelation}) = length(relation.representation.offsets) function _relation( - pair::Pair{<:Space,<:Space}, representation; + pair::Pair{<:Space, <:Space}, representation; id::UUIDs.UUID, schema_epoch::Integer, ) epoch = _checked_schema_epoch(schema_epoch) @@ -350,33 +372,41 @@ function _canonical_affine_offset(offset, dimensions::Int) values = try Tuple(offset) catch - throw(LocalMathValidationError( - "an affine offset must be an integer coordinate tuple"; - stage = :construct, contract = :affine_offset_type, - expected = :integer_tuple, actual = typeof(offset), - )) - end - length(values) == dimensions || throw(LocalMathValidationError( - "an affine offset dimensionality does not match its source Space"; - stage = :construct, contract = :affine_offset_dimension, - expected = dimensions, actual = length(values), - )) - all(value -> value isa Integer, values) || throw(LocalMathValidationError( - "affine offsets must contain integers"; - stage = :construct, contract = :affine_offset_type, - expected = Integer, actual = typeof(offset), - )) - return ntuple(axis -> begin - value = Int(values[axis]) - typemin(Int32) <= value <= typemax(Int32) || throw( + throw( LocalMathValidationError( - "an affine offset exceeds the Int32 execution ABI"; - stage = :construct, contract = :affine_offset_value, - expected = typemin(Int32):typemax(Int32), actual = value, + "an affine offset must be an integer coordinate tuple"; + stage = :construct, contract = :affine_offset_type, + expected = :integer_tuple, actual = typeof(offset), ) ) - value - end, dimensions) + end + length(values) == dimensions || throw( + LocalMathValidationError( + "an affine offset dimensionality does not match its source Space"; + stage = :construct, contract = :affine_offset_dimension, + expected = dimensions, actual = length(values), + ) + ) + all(value -> value isa Integer, values) || throw( + LocalMathValidationError( + "affine offsets must contain integers"; + stage = :construct, contract = :affine_offset_type, + expected = Integer, actual = typeof(offset), + ) + ) + return ntuple( + axis -> begin + value = Int(values[axis]) + typemin(Int32) <= value <= typemax(Int32) || throw( + LocalMathValidationError( + "an affine offset exceeds the Int32 execution ABI"; + stage = :construct, contract = :affine_offset_value, + expected = typemin(Int32):typemax(Int32), actual = value, + ) + ) + value + end, dimensions + ) end """ @@ -385,33 +415,41 @@ end Construct a storage-free fixed Cartesian offset gather. """ function AffineRelation( - pair::Pair{<:Space,<:Space}; offsets, + pair::Pair{<:Space, <:Space}; offsets, origin = nothing, id::UUIDs.UUID = _new_semantic_identity(), schema_epoch::Integer = 0, ) ndims = length(size(first(pair))) - length(size(last(pair))) == ndims || throw(LocalMathValidationError( - "an affine relation requires equal domain/codomain dimensionality"; - stage = :construct, contract = :affine_endpoint_dimension, - expected = ndims, actual = length(size(last(pair))), - )) + length(size(last(pair))) == ndims || throw( + LocalMathValidationError( + "an affine relation requires equal domain/codomain dimensionality"; + stage = :construct, contract = :affine_endpoint_dimension, + expected = ndims, actual = length(size(last(pair))), + ) + ) canonical = Tuple( _canonical_affine_offset(offset, ndims) for offset in offsets ) - isempty(canonical) && throw(LocalMathValidationError( - "an affine relation requires at least one offset"; - stage = :construct, contract = :affine_degree_bound, - expected = :positive, actual = 0, - )) - length(canonical) <= 32 || throw(LocalMathValidationError( - "an affine relation is a small static stencil with at most 32 lanes"; - stage = :construct, contract = :affine_degree_bound, - expected = 1:32, actual = length(canonical), - )) + isempty(canonical) && throw( + LocalMathValidationError( + "an affine relation requires at least one offset"; + stage = :construct, contract = :affine_degree_bound, + expected = :positive, actual = 0, + ) + ) + length(canonical) <= 32 || throw( + LocalMathValidationError( + "an affine relation is a small static stencil with at most 32 lanes"; + stage = :construct, contract = :affine_degree_bound, + expected = 1:32, actual = length(canonical), + ) + ) canonical_origin = origin === nothing ? ntuple(_ -> 0, ndims) : _canonical_affine_offset(origin, ndims) - return _relation(pair, _AffineRelation(canonical, canonical_origin); - id, schema_epoch) + return _relation( + pair, _AffineRelation(canonical, canonical_origin); + id, schema_epoch + ) end """ @@ -421,7 +459,7 @@ Declare stored fixed-degree topology. Bind `:endpoints` in lane-major order and optionally `:counts` for incomplete rows. """ function FixedRelation( - pair::Pair{<:Space,<:Space}; degree::Integer, + pair::Pair{<:Space, <:Space}; degree::Integer, id::UUIDs.UUID = _new_semantic_identity(), schema_epoch::Integer = 0, ) representation = _FixedRelation( @@ -438,19 +476,23 @@ product `Space`s whose factors match the relation domains and codomains. Lane degree is the product of factor degrees. """ function ProductRelation( - pair::Pair{<:Space,<:Space}, factors::Tuple; + pair::Pair{<:Space, <:Space}, factors::Tuple; id::UUIDs.UUID = _new_semantic_identity(), schema_epoch::Integer = 0, ) - isempty(factors) && throw(LocalMathValidationError( - "a product relation requires at least one factor"; - stage = :construct, contract = :product_relation_factors, - expected = :nonempty_relation_tuple, actual = factors, - )) - all(factor -> factor isa Relation, factors) || throw(LocalMathValidationError( - "every product factor must be a Relation"; - stage = :construct, contract = :product_relation_factors, - expected = Relation, actual = map(typeof, factors), - )) + isempty(factors) && throw( + LocalMathValidationError( + "a product relation requires at least one factor"; + stage = :construct, contract = :product_relation_factors, + expected = :nonempty_relation_tuple, actual = factors, + ) + ) + all(factor -> factor isa Relation, factors) || throw( + LocalMathValidationError( + "every product factor must be a Relation"; + stage = :construct, contract = :product_relation_factors, + expected = Relation, actual = map(typeof, factors), + ) + ) first(pair) isa Space{_ProductSpaceKind} && last(pair) isa Space{_ProductSpaceKind} || throw( LocalMathValidationError( @@ -493,27 +535,33 @@ function compose( stage = :construct, contract = :composed_relation_adjacency, expected = codomain(factors[index]), actual = domain(factors[index + 1]), - )) + ) + ) end degree = _checked_semantic_product( - map(degree_bound, factors), :composed_relation_degree) - degree <= _MAX_STATIC_COMPOSED_RELATION_DEGREE || throw(LocalMathValidationError( - "a composed Relation degree product must fit the static Stage lane bound"; - stage = :construct, contract = :composed_relation_degree, - expected = 1:_MAX_STATIC_COMPOSED_RELATION_DEGREE, actual = degree, - )) - return _relation(domain(first_relation) => codomain(last(factors)), - _ComposedRelation(factors, degree); id, schema_epoch) + map(degree_bound, factors), :composed_relation_degree + ) + degree <= _MAX_STATIC_COMPOSED_RELATION_DEGREE || throw( + LocalMathValidationError( + "a composed Relation degree product must fit the static Stage lane bound"; + stage = :construct, contract = :composed_relation_degree, + expected = 1:_MAX_STATIC_COMPOSED_RELATION_DEGREE, actual = degree, + ) + ) + return _relation( + domain(first_relation) => codomain(last(factors)), + _ComposedRelation(factors, degree); id, schema_epoch + ) end """`MaskedBoundary(mask, fallback)` makes masked lanes absent before applying `fallback`.""" function MaskedBoundary(mask::Field{Bool}, fallback::_BoundaryPolicy) - return MaskedBoundary{typeof(mask),typeof(fallback)}(mask, fallback) + return MaskedBoundary{typeof(mask), typeof(fallback)}(mask, fallback) end """`GhostBoundary(lower, upper, ghost_space)` routes exterior lanes to explicit ghost storage.""" function GhostBoundary( - lower::Tuple{Vararg{Integer,D}}, - upper::Tuple{Vararg{Integer,D}}, + lower::Tuple{Vararg{Integer, D}}, + upper::Tuple{Vararg{Integer, D}}, ghost_space::Space, ) where {D} canonical_lower = ntuple( @@ -528,11 +576,13 @@ end _validate_boundary_policy(base::Relation, ::StrictBoundary) = nothing _validate_boundary_policy(base::Relation, ::ExteriorBoundary) = nothing function _validate_boundary_policy(base::Relation, policy::PeriodicBoundary{D}) where {D} - length(size(codomain(base))) == D || throw(LocalMathValidationError( - "periodic axes do not match the relation dimensionality"; - stage = :construct, contract = :periodic_boundary_dimension, - expected = length(size(codomain(base))), actual = D, - )) + length(size(codomain(base))) == D || throw( + LocalMathValidationError( + "periodic axes do not match the relation dimensionality"; + stage = :construct, contract = :periodic_boundary_dimension, + expected = length(size(codomain(base))), actual = D, + ) + ) for axis in 1:D policy.axes[axis] && size(codomain(base))[axis] == 0 && throw( LocalMathValidationError( @@ -545,21 +595,25 @@ function _validate_boundary_policy(base::Relation, policy::PeriodicBoundary{D}) return nothing end function _validate_boundary_policy(base::Relation, policy::MaskedBoundary) - policy.mask.space == codomain(base) || throw(LocalMathValidationError( - "a boundary mask must be placed on the relation codomain"; - stage = :construct, contract = :masked_boundary_space, - expected = semantic_identity(codomain(base)), - actual = semantic_identity(policy.mask.space), - )) + policy.mask.space == codomain(base) || throw( + LocalMathValidationError( + "a boundary mask must be placed on the relation codomain"; + stage = :construct, contract = :masked_boundary_space, + expected = semantic_identity(codomain(base)), + actual = semantic_identity(policy.mask.space), + ) + ) _validate_boundary_policy(base, policy.fallback) return nothing end function _validate_boundary_policy(base::Relation, policy::GhostBoundary{D}) where {D} - length(size(codomain(base))) == D || throw(LocalMathValidationError( - "ghost depth does not match the relation dimensionality"; - stage = :construct, contract = :ghost_boundary_dimension, - expected = length(size(codomain(base))), actual = D, - )) + length(size(codomain(base))) == D || throw( + LocalMathValidationError( + "ghost depth does not match the relation dimensionality"; + stage = :construct, contract = :ghost_boundary_dimension, + expected = length(size(codomain(base))), actual = D, + ) + ) padded = ntuple( axis -> size(codomain(base))[axis] + policy.lower[axis] + policy.upper[axis], @@ -599,15 +653,17 @@ Declare storage-free bounded evaluator-provided routing using one-based `Int32` or `UInt32` keys. Key zero denotes absence. """ function RuntimeRelation( - pair::Pair{<:Space,<:Space}; degree_bound::Integer, + pair::Pair{<:Space, <:Space}; degree_bound::Integer, key_type::Type{K}, ownership::Symbol = :local, id::UUIDs.UUID = _new_semantic_identity(), schema_epoch::Integer = 0, ) where {K} - K in (Int32, UInt32) || throw(LocalMathValidationError( - "a runtime relation currently uses one-based Int32/UInt32 ordinal keys"; - stage = :construct, contract = :runtime_relation_key_type, - expected = (Int32, UInt32), actual = K, - )) + K in (Int32, UInt32) || throw( + LocalMathValidationError( + "a runtime relation currently uses one-based Int32/UInt32 ordinal keys"; + stage = :construct, contract = :runtime_relation_key_type, + expected = (Int32, UInt32), actual = K, + ) + ) representation = _RuntimeRelation{K}( _checked_semantic_int( degree_bound, :runtime_relation_degree; positive = true @@ -618,13 +674,13 @@ function RuntimeRelation( return _relation(pair, representation; id, schema_epoch) end -_index_key_degree(::Type{K}) where {K<:Integer} = 1 -function _index_key_degree(::Type{K}) where {K<:Tuple} +_index_key_degree(::Type{K}) where {K <: Integer} = 1 +function _index_key_degree(::Type{K}) where {K <: Tuple} types = fieldtypes(K) !isempty(types) && all(T -> T <: Integer && T !== Bool, types) || return 0 return length(types) end -function _index_key_degree(::Type{K}) where {K<:StaticArrays.StaticVector} +function _index_key_degree(::Type{K}) where {K <: StaticArrays.StaticVector} eltype(K) <: Integer && eltype(K) !== Bool || return 0 return length(K) end @@ -639,16 +695,18 @@ lane degree. Strict relations reject out-of-range keys during execution, whereas optional relations expose those lanes as absent samples. """ function IndexRelation( - pair::Pair{<:Field,<:Space}; optional::Bool = false, + pair::Pair{<:Field, <:Space}; optional::Bool = false, id::UUIDs.UUID = _new_semantic_identity(), schema_epoch::Integer = 0, ) keys = first(pair) degree = _index_key_degree(eltype(keys)) - 1 <= degree <= 32 || throw(LocalMathValidationError( - "IndexRelation keys must be integer scalars or fixed-width integer tuples/static vectors"; - stage = :construct, contract = :index_relation_key_type, - expected = :bounded_integer_key, actual = eltype(keys), - )) + 1 <= degree <= 32 || throw( + LocalMathValidationError( + "IndexRelation keys must be integer scalars or fixed-width integer tuples/static vectors"; + stage = :construct, contract = :index_relation_key_type, + expected = :bounded_integer_key, actual = eltype(keys), + ) + ) representation = _FieldIndexRelation(keys, degree, optional) return _relation(keys.space => last(pair), representation; id, schema_epoch) end @@ -659,12 +717,14 @@ function MaskedRelation( id::UUIDs.UUID = _new_semantic_identity(), schema_epoch::Integer = schema_epoch(base), ) - mask.space == domain(base) || throw(LocalMathValidationError( - "a source mask must be placed on the base relation domain"; - stage = :construct, contract = :masked_relation_domain, - expected = semantic_identity(domain(base)), - actual = semantic_identity(mask.space), - )) + mask.space == domain(base) || throw( + LocalMathValidationError( + "a source mask must be placed on the base relation domain"; + stage = :construct, contract = :masked_relation_domain, + expected = semantic_identity(domain(base)), + actual = semantic_identity(mask.space), + ) + ) return _relation( domain(base) => codomain(base), _MaskedRelation(base, mask, degree_bound(base)); id, schema_epoch, @@ -677,12 +737,14 @@ function SelectedRelation( id::UUIDs.UUID = _new_semantic_identity(), schema_epoch::Integer = max(schema_epoch(base), schema_epoch(injection)), ) - codomain(injection) == domain(base) || throw(LocalMathValidationError( - "a selection injection must map its selected Space into the base domain"; - stage = :construct, contract = :selected_relation_injection, - expected = semantic_identity(domain(base)), - actual = semantic_identity(codomain(injection)), - )) + codomain(injection) == domain(base) || throw( + LocalMathValidationError( + "a selection injection must map its selected Space into the base domain"; + stage = :construct, contract = :selected_relation_injection, + expected = semantic_identity(domain(base)), + actual = semantic_identity(codomain(injection)), + ) + ) degree = _checked_semantic_product( (degree_bound(injection), degree_bound(base)), :selected_relation_degree, @@ -723,7 +785,7 @@ Declare generation-qualified mutable packed topology with an explicit degree bound and total endpoint capacity. """ function PackedRelation( - pair::Pair{<:Space,<:Space}; degree_bound::Integer, + pair::Pair{<:Space, <:Space}; degree_bound::Integer, capacity::Integer, layout::Symbol = :bounded_columns, ownership::Symbol = :local, id::UUIDs.UUID = _new_semantic_identity(), schema_epoch::Integer = 0, @@ -751,7 +813,7 @@ end mutable struct _RelationProofSeal end const _RELATION_PROOF_SEAL = _RelationProofSeal() -struct _ValidatedRelationEvidence{B,M,C,O,H} +struct _ValidatedRelationEvidence{B, M, C, O, H} bounds::B multiplicity::M coverage::C @@ -761,17 +823,19 @@ struct _ValidatedRelationEvidence{B,M,C,O,H} function _ValidatedRelationEvidence( seal::_RelationProofSeal, bounds::B, multiplicity::M, coverage::C, canonical_order::O, footprint::H, - ) where {B,M,C,O,H} - seal === _RELATION_PROOF_SEAL || throw(ArgumentError( - "validated relation evidence requires the planner-owned seal" - )) - return new{B,M,C,O,H}( + ) where {B, M, C, O, H} + seal === _RELATION_PROOF_SEAL || throw( + ArgumentError( + "validated relation evidence requires the planner-owned seal" + ) + ) + return new{B, M, C, O, H}( bounds, multiplicity, coverage, canonical_order, footprint, ) end end -struct RelationProof{S,E} +struct RelationProof{S, E} relation_id::UUIDs.UUID domain_id::UUIDs.UUID codomain_id::UUIDs.UUID @@ -783,10 +847,12 @@ struct RelationProof{S,E} seal::_RelationProofSeal, relation::Relation, binding_schema::S, evidence::_ValidatedRelationEvidence, ) where {S} - seal === _RELATION_PROOF_SEAL || throw(ArgumentError( - "RelationProof construction requires the planner-owned seal" - )) - return new{S,typeof(evidence)}( + seal === _RELATION_PROOF_SEAL || throw( + ArgumentError( + "RelationProof construction requires the planner-owned seal" + ) + ) + return new{S, typeof(evidence)}( semantic_identity(relation), semantic_identity(domain(relation)), semantic_identity(codomain(relation)), diff --git a/src/stage_model.jl b/src/stage_model.jl index 362738e..5334992 100644 --- a/src/stage_model.jl +++ b/src/stage_model.jl @@ -19,15 +19,19 @@ struct _ClosedParameterBounds{T} <: _ParameterBounds seal::_StageModelSeal, lower::T, upper::T ) where {T} seal === _STAGE_MODEL_SEAL || error("invalid stage-model seal") - _storage_value_type(T) || throw(LocalMathValidationError( - "parameter bounds require an admitted storage value type"; - stage = :construct, contract = :parameter_bounds_type, actual = T, - )) - lower <= upper || throw(LocalMathValidationError( - "parameter bounds must be ordered"; - stage = :construct, contract = :parameter_bounds, - expected = :ordered, actual = (lower, upper), - )) + _storage_value_type(T) || throw( + LocalMathValidationError( + "parameter bounds require an admitted storage value type"; + stage = :construct, contract = :parameter_bounds_type, actual = T, + ) + ) + lower <= upper || throw( + LocalMathValidationError( + "parameter bounds must be ordered"; + stage = :construct, contract = :parameter_bounds, + expected = :ordered, actual = (lower, upper), + ) + ) return new{T}(lower, upper) end end @@ -35,21 +39,21 @@ _ClosedParameterBounds(lower, upper) = _ClosedParameterBounds(_STAGE_MODEL_SEAL, lower, upper) """`Parameter(name, T; bounds=nothing)` declares one typed submission parameter.""" -struct Parameter{T,B<:_ParameterBounds} +struct Parameter{T, B <: _ParameterBounds} name::Symbol bounds::B function Parameter( seal::_StageModelSeal, ::Type{T}, name::Symbol, bounds::B - ) where {T,B<:_ParameterBounds} + ) where {T, B <: _ParameterBounds} seal === _STAGE_MODEL_SEAL || error("invalid stage-model seal") - bounds isa Union{_UnboundedParameter,_ClosedParameterBounds} || throw( + bounds isa Union{_UnboundedParameter, _ClosedParameterBounds} || throw( LocalMathValidationError( "foreign parameter-bound laws are not admitted"; stage = :construct, contract = :parameter_bounds, actual = B, ) ) - return new{T,B}(name, bounds) + return new{T, B}(name, bounds) end end @@ -62,15 +66,15 @@ function _device_type_parameter(parameter) # still the final authority over the method body. parameter isa Symbol && return true parameter === nothing && return true - parameter isa Union{UUIDs.UUID,Val,Ptr,Ref,NamedTuple} && return false + parameter isa Union{UUIDs.UUID, Val, Ptr, Ref, NamedTuple} && return false parameter isa Bool && return true parameter isa Enum && return true parameter isa Integer && return 0 <= parameter <= 32 parameter isa Tuple && return all(_device_type_parameter, parameter) if parameter isa Type - parameter <: Union{Ptr,Ref,AbstractArray,NamedTuple,Val,Space,Field,Relation} && - return false _storage_value_type(parameter) && return true + parameter <: Union{Ptr, Ref, AbstractArray, NamedTuple, Val, Space, Field, Relation} && + return false return isconcretetype(parameter) && isbitstype(parameter) && _device_type_parameters(parameter) end @@ -86,37 +90,51 @@ function _device_evaluator_capture(value) T = typeof(value) _device_parameter_type(T) && return true value isa Union{ - Symbol,UUIDs.UUID,Val,Ptr,Ref,AbstractArray,NamedTuple, - Space,Field,Relation, + Symbol, UUIDs.UUID, Val, Ptr, Ref, AbstractArray, NamedTuple, + Space, Field, Relation, } && return false isconcretetype(T) && isbitstype(T) || return false value isa Function && fieldcount(T) == 0 && return true _device_type_parameters(T) || return false - return all(index -> _device_evaluator_capture(getfield(value, index)), - 1:fieldcount(T)) + return all( + index -> _device_evaluator_capture(getfield(value, index)), + 1:fieldcount(T) + ) end function _device_capture_rejection(value; path::Tuple = (:evaluator,)) T = typeof(value) _device_parameter_type(T) && return nothing if value isa AbstractArray - return (path, type = T, reason = :array_capture, - hint = "declare the array as a Field and gather it explicitly") + return ( + path, type = T, reason = :array_capture, + hint = "declare the array as a Field and gather it explicitly", + ) elseif value isa Symbol - return (path, type = T, reason = :runtime_symbol, - hint = "put symbolic identity in the callable type, not a runtime field") - elseif value isa Union{Ref,Ptr} - return (path, type = T, reason = :reference_capture, - hint = "pass immutable scalar data or declare device storage explicitly") - elseif value isa Union{Space,Field,Relation} - return (path, type = T, reason = :descriptor_capture, - hint = "declare the descriptor as a Stage access instead of capturing it") - elseif value isa Union{UUIDs.UUID,Val,NamedTuple} - return (path, type = T, reason = :unsupported_capture, - hint = "capture only concrete isbits scalar or tuple values") + return ( + path, type = T, reason = :runtime_symbol, + hint = "put symbolic identity in the callable type, not a runtime field", + ) + elseif value isa Union{Ref, Ptr} + return ( + path, type = T, reason = :reference_capture, + hint = "pass immutable scalar data or declare device storage explicitly", + ) + elseif value isa Union{Space, Field, Relation} + return ( + path, type = T, reason = :descriptor_capture, + hint = "declare the descriptor as a Stage access instead of capturing it", + ) + elseif value isa Union{UUIDs.UUID, Val, NamedTuple} + return ( + path, type = T, reason = :unsupported_capture, + hint = "capture only concrete isbits scalar or tuple values", + ) elseif !isconcretetype(T) - return (path, type = T, reason = :nonconcrete_capture, - hint = "use a concrete callable type") + return ( + path, type = T, reason = :nonconcrete_capture, + hint = "use a concrete callable type", + ) end # Immutable callable wrappers commonly become non-isbits because one of # their captures is not device-safe. Report that scientific capture rather @@ -124,17 +142,23 @@ function _device_capture_rejection(value; path::Tuple = (:evaluator,)) if !ismutabletype(T) for index in 1:fieldcount(T) name = fieldname(T, index) - rejected = _device_capture_rejection(getfield(value, index); - path = (path..., name)) + rejected = _device_capture_rejection( + getfield(value, index); + path = (path..., name) + ) rejected === nothing || return rejected end end if !isbitstype(T) - return (path, type = T, reason = :mutable_or_nonisbits_capture, - hint = "store mutable data in Fields and capture only immutable isbits values") + return ( + path, type = T, reason = :mutable_or_nonisbits_capture, + hint = "store mutable data in Fields and capture only immutable isbits values", + ) elseif !_device_type_parameters(T) - return (path, type = T, reason = :unsafe_type_parameter, - hint = "use device-value type parameters and compile-time Symbol identities only") + return ( + path, type = T, reason = :unsafe_type_parameter, + hint = "use device-value type parameters and compile-time Symbol identities only", + ) end return nothing end @@ -142,7 +166,8 @@ end function _device_callable_rejection(value; path::Tuple) _has_call_methods(value) || return ( path, type = typeof(value), reason = :not_callable, - hint = "pass an ordinary callable struct or function") + hint = "pass an ordinary callable struct or function", + ) return _device_capture_rejection(value; path) end @@ -170,23 +195,28 @@ function Parameter( ) where {T} bounds = bounds === nothing ? _UnboundedParameter() : bounds isa Tuple && length(bounds) == 2 ? - _ClosedParameterBounds(bounds[1], bounds[2]) : bounds - isempty(String(name)) && throw(LocalMathValidationError( - "a parameter name must be nonempty"; - stage = :construct, contract = :parameter_name, - )) - _device_parameter_type(T) || throw(LocalMathValidationError( - "a parameter requires an admitted device-value type"; - stage = :construct, contract = :parameter_type, - expected = :numeric_bool_enum_tuple_or_static_array, actual = T, - )) - bounds isa Union{_UnboundedParameter,_ClosedParameterBounds} || throw( + _ClosedParameterBounds(bounds[1], bounds[2]) : bounds + isempty(String(name)) && throw( + LocalMathValidationError( + "a parameter name must be nonempty"; + stage = :construct, contract = :parameter_name, + ) + ) + _device_parameter_type(T) || throw( + LocalMathValidationError( + "a parameter requires an admitted device-value type"; + stage = :construct, contract = :parameter_type, + expected = :numeric_bool_enum_tuple_or_static_array, actual = T, + ) + ) + bounds isa Union{_UnboundedParameter, _ClosedParameterBounds} || throw( LocalMathValidationError( - "parameter bounds must be a closed package-owned law"; - stage = :construct, contract = :parameter_bounds, - expected = Union{_UnboundedParameter,_ClosedParameterBounds}, - actual = typeof(bounds), - )) + "parameter bounds must be a closed package-owned law"; + stage = :construct, contract = :parameter_bounds, + expected = Union{_UnboundedParameter, _ClosedParameterBounds}, + actual = typeof(bounds), + ) + ) bounds isa _ClosedParameterBounds && typeof(bounds.lower) !== T && throw( LocalMathValidationError( "parameter bounds must have the declared parameter type"; @@ -202,14 +232,16 @@ end _parameter_type(::Parameter{T}) where {T} = T """Cold exact parameter declarations; names are values, never type keys.""" -struct ParameterSchema{D<:Tuple} +struct ParameterSchema{D <: Tuple} declarations::D - function ParameterSchema(declarations::D) where {D<:Tuple} + function ParameterSchema(declarations::D) where {D <: Tuple} all(declaration -> declaration isa Parameter, declarations) || - throw(LocalMathValidationError( + throw( + LocalMathValidationError( "ParameterSchema accepts only typed parameter declarations"; stage = :construct, contract = :parameter_schema, - )) + ) + ) names = map(declaration -> declaration.name, declarations) length(unique(names)) == length(names) || throw( LocalMathValidationError( @@ -227,18 +259,19 @@ ParameterSchema(declarations::Parameter...) = ParameterSchema(declarations) """Host-only positional submission metadata; parameter names remain values.""" -struct _StageParameterSlot{T,B<:_ParameterBounds} +struct _StageParameterSlot{T, B <: _ParameterBounds} name::Symbol bounds::B end -struct _StageParameterLayout{S<:Tuple} +struct _StageParameterLayout{S <: Tuple} slots::S end @inline _stage_parameter_slot( - declaration::Parameter{T,B}) where {T,B} = - _StageParameterSlot{T,B}(declaration.name, declaration.bounds) + declaration::Parameter{T, B} +) where {T, B} = + _StageParameterSlot{T, B}(declaration.name, declaration.bounds) _stage_parameter_layout(schema::ParameterSchema) = _StageParameterLayout(map(_stage_parameter_slot, schema.declarations)) @@ -250,10 +283,12 @@ _stage_parameter_layout(schema::ParameterSchema) = function _merge_parameter_schemas(schemas::Tuple) merged = () for schema in schemas - schema isa ParameterSchema || throw(LocalMathValidationError( - "parameter schema composition requires ParameterSchema values"; - stage = :construct, contract = :parameter_schema_composition, - )) + schema isa ParameterSchema || throw( + LocalMathValidationError( + "parameter schema composition requires ParameterSchema values"; + stage = :construct, contract = :parameter_schema_composition, + ) + ) for declaration in schema.declarations position = findfirst( existing -> existing.name === declaration.name, merged @@ -264,13 +299,13 @@ function _merge_parameter_schemas(schemas::Tuple) existing = merged[position] typeof(existing) === typeof(declaration) && existing.bounds == declaration.bounds || throw( - LocalMathValidationError( - "repeated parameter declarations must agree exactly"; - stage = :construct, - contract = :parameter_schema_composition, - expected = existing, actual = declaration, - ) + LocalMathValidationError( + "repeated parameter declarations must agree exactly"; + stage = :construct, + contract = :parameter_schema_composition, + expected = existing, actual = declaration, ) + ) end end end @@ -280,22 +315,26 @@ end struct _ParameterSlot{N} end """`Evaluator(callable, parameters=())` declares one concrete isbits stage calculation.""" -struct Evaluator{E,P<:Tuple} +struct Evaluator{E, P <: Tuple} evaluator::E parameters::P - function Evaluator(evaluator::E, parameters::P = ()) where {E,P<:Tuple} + function Evaluator(evaluator::E, parameters::P = ()) where {E, P <: Tuple} isconcretetype(E) && isbitstype(E) && _has_call_methods(evaluator) && _device_evaluator_capture(evaluator) || - throw(LocalMathValidationError( - "a stage evaluator must be one concrete structurally device-admissible callable value"; - stage = :construct, contract = :stage_evaluator, - expected = :device_safe_callable, - actual = _device_callable_rejection(evaluator; - path = (:evaluator,)), - hint = "move array or descriptor state into declared Stage accesses", - )) + throw( + LocalMathValidationError( + "a stage evaluator must be one concrete structurally device-admissible callable value"; + stage = :construct, contract = :stage_evaluator, + expected = :device_safe_callable, + actual = _device_callable_rejection( + evaluator; + path = (:evaluator,) + ), + hint = "move array or descriptor state into declared Stage accesses", + ) + ) ParameterSchema(parameters) - return new{E,P}(evaluator, parameters) + return new{E, P}(evaluator, parameters) end end @@ -319,21 +358,29 @@ function _relation_ghost_space(representation::_SelectedRelation) return _relation_ghost_space(representation.base) end function _relation_ghost_space(representation::_ProductRelation) - any(factor -> _relation_ghost_space(factor) !== nothing, - representation.factors) && throw(LocalMathValidationError( - "a product Relation does not admit a ghost-boundary factor"; - stage = :construct, contract = :product_ghost_boundary, - expected = :explicit_product_halo_law, - )) + any( + factor -> _relation_ghost_space(factor) !== nothing, + representation.factors + ) && throw( + LocalMathValidationError( + "a product Relation does not admit a ghost-boundary factor"; + stage = :construct, contract = :product_ghost_boundary, + expected = :explicit_product_halo_law, + ) + ) return nothing end function _relation_ghost_space(representation::_ComposedRelation) - any(factor -> _relation_ghost_space(factor) !== nothing, - representation.factors) && throw(LocalMathValidationError( - "composed ghost Relations require an explicit halo law"; - stage = :construct, contract = :composed_ghost_boundary, - expected = :explicit_composed_halo_law, - )) + any( + factor -> _relation_ghost_space(factor) !== nothing, + representation.factors + ) && throw( + LocalMathValidationError( + "composed ghost Relations require an explicit halo law"; + stage = :construct, contract = :composed_ghost_boundary, + expected = :explicit_composed_halo_law, + ) + ) return nothing end @@ -341,7 +388,7 @@ abstract type _AccessMode end struct _SampleAccess <: _AccessMode end struct _RequiredAccess <: _AccessMode end -struct Access{F<:Field,R<:Relation,V,G,M<:_AccessMode} +struct Access{F <: Field, R <: Relation, V, G, M <: _AccessMode} field::F relation::R version::V @@ -349,34 +396,42 @@ struct Access{F<:Field,R<:Relation,V,G,M<:_AccessMode} mode::M function Access( field::F, relation::R, version::V, ghost::G, mode::M, - ) where {F<:Field,R<:Relation,V,G,M<:_AccessMode} - version isa _StageEntryVersion || throw(LocalMathValidationError( - "Field access is admitted only at stage entry"; - stage = :construct, contract = :access_version, - expected = _StageEntryVersion, actual = V, - )) - codomain(relation) == field.space || throw(LocalMathValidationError( - "an access Relation must terminate at the accessed Field Space"; - stage = :construct, contract = :access_codomain, - expected = field.space, actual = codomain(relation), - )) + ) where {F <: Field, R <: Relation, V, G, M <: _AccessMode} + version isa _StageEntryVersion || throw( + LocalMathValidationError( + "Field access is admitted only at stage entry"; + stage = :construct, contract = :access_version, + expected = _StageEntryVersion, actual = V, + ) + ) + codomain(relation) == field.space || throw( + LocalMathValidationError( + "an access Relation must terminate at the accessed Field Space"; + stage = :construct, contract = :access_codomain, + expected = field.space, actual = codomain(relation), + ) + ) ghost_space = _relation_ghost_space(relation) if ghost_space === nothing - ghost === nothing || throw(LocalMathValidationError( - "a ghost Field is valid only for a relation with a ghost boundary"; - stage = :construct, contract = :access_ghost, - actual = typeof(ghost), - )) + ghost === nothing || throw( + LocalMathValidationError( + "a ghost Field is valid only for a relation with a ghost boundary"; + stage = :construct, contract = :access_ghost, + actual = typeof(ghost), + ) + ) else ghost isa Field && ghost.space == ghost_space && - eltype(ghost) === eltype(field) || throw(LocalMathValidationError( - "a ghost-boundary access requires an explicit same-typed Field on its ghost Space"; - stage = :construct, contract = :access_ghost, - expected = (ghost_space, eltype(field)), - actual = ghost === nothing ? nothing : (ghost.space, eltype(ghost)), - )) + eltype(ghost) === eltype(field) || throw( + LocalMathValidationError( + "a ghost-boundary access requires an explicit same-typed Field on its ghost Space"; + stage = :construct, contract = :access_ghost, + expected = (ghost_space, eltype(field)), + actual = ghost === nothing ? nothing : (ghost.space, eltype(ghost)), + ) + ) end - return new{F,R,V,G,M}(field, relation, version, ghost, mode) + return new{F, R, V, G, M}(field, relation, version, ghost, mode) end end @@ -392,8 +447,10 @@ The relation must terminate at `field.space`. A `ghost` Field is accepted only for a relation carrying a `GhostBoundary` policy. """ Access(field::Field, relation::Relation; ghost = nothing, required::Bool = true) = - Access(field, relation, _StageEntryVersion(), ghost, - required ? _RequiredAccess() : _SampleAccess()) + Access( + field, relation, _StageEntryVersion(), ghost, + required ? _RequiredAccess() : _SampleAccess() +) abstract type _CollectionAccessLaw end @@ -401,30 +458,39 @@ abstract type _CollectionAccessLaw end struct _BoundedGroup{K} <: _CollectionAccessLaw end """`BoundedGroup(maximum)` declares a static grouped-collection occupancy bound.""" function BoundedGroup(maximum::Integer) - maximum isa Bool && throw(LocalMathValidationError( - "a bounded Collection group requires an integer occupancy bound"; - stage = :construct, contract = :collection_group_bound, actual = maximum)) - 1 <= maximum <= 32 || throw(LocalMathValidationError( - "a bounded Collection group exceeds the reviewed static occupancy bound"; - stage = :construct, contract = :collection_group_bound, - expected = 1:32, actual = maximum)) + maximum isa Bool && throw( + LocalMathValidationError( + "a bounded Collection group requires an integer occupancy bound"; + stage = :construct, contract = :collection_group_bound, actual = maximum + ) + ) + 1 <= maximum <= 32 || throw( + LocalMathValidationError( + "a bounded Collection group exceeds the reviewed static occupancy bound"; + stage = :construct, contract = :collection_group_bound, + expected = 1:32, actual = maximum + ) + ) return _BoundedGroup{Int(maximum)}() end """Read one selected compacted position from the current producer item.""" -struct _SourcePositionsAccess{K,L} <: _CollectionAccessLaw end +struct _SourcePositionsAccess{K, L} <: _CollectionAccessLaw end """`CollectionAccess(collection, BoundedGroup(maximum))` reads one dense collection group.""" -struct CollectionAccess{C,L<:_CollectionAccessLaw} +struct CollectionAccess{C, L <: _CollectionAccessLaw} collection::C law::L end function CollectionAccess(collection, law::_BoundedGroup) - collection isa Collection || throw(LocalMathValidationError( - "a Collection access requires a Collection descriptor"; - stage = :construct, contract = :collection_access_descriptor, - expected = Collection, actual = typeof(collection))) - return CollectionAccess{typeof(collection),typeof(law)}(collection, law) + collection isa Collection || throw( + LocalMathValidationError( + "a Collection access requires a Collection descriptor"; + stage = :construct, contract = :collection_access_descriptor, + expected = Collection, actual = typeof(collection) + ) + ) + return CollectionAccess{typeof(collection), typeof(law)}(collection, law) end """ @@ -435,21 +501,31 @@ The producing `Collect` must request `persistent_source_position()`. Planning resolves the producer's full emission width and rejects a lane outside it. """ function SourcePositionAccess(collection, lane::Integer = 1) - collection isa Collection || throw(LocalMathValidationError( - "a source-position access requires a Collection descriptor"; - stage = :construct, contract = :collection_access_descriptor, - expected = Collection, actual = typeof(collection))) - lane isa Bool && throw(LocalMathValidationError( - "a source-position Collection access lane must be an integer"; - stage = :construct, contract = :collection_source_position_lane, - actual = lane)) - 1 <= lane <= 32 || throw(LocalMathValidationError( - "a source-position Collection access exceeds the reviewed static lane bound"; - stage = :construct, contract = :collection_source_position_lane, - expected = 1:32, actual = lane)) + collection isa Collection || throw( + LocalMathValidationError( + "a source-position access requires a Collection descriptor"; + stage = :construct, contract = :collection_access_descriptor, + expected = Collection, actual = typeof(collection) + ) + ) + lane isa Bool && throw( + LocalMathValidationError( + "a source-position Collection access lane must be an integer"; + stage = :construct, contract = :collection_source_position_lane, + actual = lane + ) + ) + 1 <= lane <= 32 || throw( + LocalMathValidationError( + "a source-position Collection access exceeds the reviewed static lane bound"; + stage = :construct, contract = :collection_source_position_lane, + expected = 1:32, actual = lane + ) + ) L = Int(lane) - return CollectionAccess{typeof(collection),_SourcePositionsAccess{0,L}}( - collection, _SourcePositionsAccess{0,L}()) + return CollectionAccess{typeof(collection), _SourcePositionsAccess{0, L}}( + collection, _SourcePositionsAccess{0, L}() + ) end """Device-resident live Collection count used as a Stage prefix.""" @@ -458,52 +534,61 @@ struct _CollectionCount{C} end """`CollectionCount(collection)` uses the device-resident live record count as a stage prefix.""" function CollectionCount(collection) - collection isa Collection || throw(LocalMathValidationError( - "a Collection count requires a Collection descriptor"; - stage = :construct, contract = :collection_count_descriptor, - expected = Collection, actual = typeof(collection))) + collection isa Collection || throw( + LocalMathValidationError( + "a Collection count requires a Collection descriptor"; + stage = :construct, contract = :collection_count_descriptor, + expected = Collection, actual = typeof(collection) + ) + ) return _CollectionCount(collection) end struct _NoPrefix end -struct _ParameterPrefix{D<:Parameter} +struct _ParameterPrefix{D <: Parameter} parameter::D end -struct _FieldPrefix{F<:Field} +struct _FieldPrefix{F <: Field} field::F - function _FieldPrefix(field::F) where {F<:Field} + function _FieldPrefix(field::F) where {F <: Field} length(field.space) == 1 && eltype(field) <: Integer && - eltype(field) !== Bool || throw(LocalMathValidationError( + eltype(field) !== Bool || throw( + LocalMathValidationError( "a Field prefix requires one singleton non-Bool integer Field"; stage = :construct, contract = :field_prefix, actual = (size(field.space), eltype(field)), - )) + ) + ) return new{F}(field) end end struct _NoMask end -struct _MaskSelection{F<:Field} +struct _MaskSelection{F <: Field} field::F - function _MaskSelection(field::F) where {F<:Field} - eltype(field) === Bool || throw(LocalMathValidationError( - "a mask selection requires a Boolean Field"; - stage = :construct, contract = :mask_selection, - expected = Bool, actual = eltype(field), - )) + function _MaskSelection(field::F) where {F <: Field} + eltype(field) === Bool || throw( + LocalMathValidationError( + "a mask selection requires a Boolean Field"; + stage = :construct, contract = :mask_selection, + expected = Bool, actual = eltype(field), + ) + ) return new{F}(field) end end struct _NoSubset end -struct _SubsetSelection{R<:Relation} +struct _SubsetSelection{R <: Relation} relation::R - function _SubsetSelection(relation::R) where {R<:Relation} - degree_bound(relation) == 1 || throw(LocalMathValidationError( - "a subset selection must be unary identity-or-absent"; - stage = :construct, contract = :subset_degree, - expected = 1, actual = degree_bound(relation), - )) + function _SubsetSelection(relation::R) where {R <: Relation} + degree_bound(relation) == 1 || throw( + LocalMathValidationError( + "a subset selection must be unary identity-or-absent"; + stage = :construct, contract = :subset_degree, + expected = 1, actual = degree_bound(relation), + ) + ) _identity_or_absent_relation(relation) || throw( LocalMathValidationError( "a subset selection must preserve source identity when present"; @@ -521,20 +606,22 @@ _identity_or_absent_relation(relation::Relation{<:_MaskedRelation}) = _identity_or_absent_relation(::Relation) = false struct _NoGate end -struct _ParameterGate{D<:Parameter} +struct _ParameterGate{D <: Parameter} parameter::D - function _ParameterGate(parameter::D) where {D<:Parameter} - _parameter_type(parameter) === Bool || throw(LocalMathValidationError( - "a parameter gate requires a Boolean parameter"; - stage = :construct, contract = :parameter_gate, - expected = Bool, actual = _parameter_type(parameter), - )) + function _ParameterGate(parameter::D) where {D <: Parameter} + _parameter_type(parameter) === Bool || throw( + LocalMathValidationError( + "a parameter gate requires a Boolean parameter"; + stage = :construct, contract = :parameter_gate, + expected = Bool, actual = _parameter_type(parameter), + ) + ) return new{D}(parameter) end end -struct _FieldGate{F<:Field} +struct _FieldGate{F <: Field} field::F - function _FieldGate(field::F) where {F<:Field} + function _FieldGate(field::F) where {F <: Field} length(field.space) == 1 && eltype(field) === Bool || throw( LocalMathValidationError( "a Field gate requires one singleton Boolean Field"; @@ -547,34 +634,42 @@ struct _FieldGate{F<:Field} end """`Control(; prefix=nothing, mask=nothing, subset=nothing, gate=nothing)` limits stage participation.""" -struct Control{P,M,S,G} +struct Control{P, M, S, G} prefix::P mask::M subset::S gate::G - function Control(prefix::P, mask::M, subset::S, gate::G) where {P,M,S,G} - prefix isa Union{_NoPrefix,_ParameterPrefix,_FieldPrefix,_CollectionCount} || throw( - LocalMathValidationError("invalid Control prefix"; - stage = :construct, contract = :control_prefix, actual = P) + function Control(prefix::P, mask::M, subset::S, gate::G) where {P, M, S, G} + prefix isa Union{_NoPrefix, _ParameterPrefix, _FieldPrefix, _CollectionCount} || throw( + LocalMathValidationError( + "invalid Control prefix"; + stage = :construct, contract = :control_prefix, actual = P + ) ) - mask isa Union{_NoMask,_MaskSelection} || throw( - LocalMathValidationError("invalid Control mask"; - stage = :construct, contract = :control_mask, actual = M) + mask isa Union{_NoMask, _MaskSelection} || throw( + LocalMathValidationError( + "invalid Control mask"; + stage = :construct, contract = :control_mask, actual = M + ) ) - subset isa Union{_NoSubset,_SubsetSelection} || throw( - LocalMathValidationError("invalid Control subset"; - stage = :construct, contract = :control_subset, actual = S) + subset isa Union{_NoSubset, _SubsetSelection} || throw( + LocalMathValidationError( + "invalid Control subset"; + stage = :construct, contract = :control_subset, actual = S + ) ) - gate isa Union{_NoGate,_ParameterGate,_FieldGate} || throw( - LocalMathValidationError("invalid Control gate"; - stage = :construct, contract = :control_gate, actual = G) + gate isa Union{_NoGate, _ParameterGate, _FieldGate} || throw( + LocalMathValidationError( + "invalid Control gate"; + stage = :construct, contract = :control_gate, actual = G + ) ) - return new{P,M,S,G}(prefix, mask, subset, gate) + return new{P, M, S, G}(prefix, mask, subset, gate) end end _control_prefix(value::_NoPrefix) = value -_control_prefix(value::Union{_ParameterPrefix,_FieldPrefix,_CollectionCount}) = value +_control_prefix(value::Union{_ParameterPrefix, _FieldPrefix, _CollectionCount}) = value _control_prefix(::Nothing) = _NoPrefix() _control_prefix(value::Parameter) = _ParameterPrefix(value) _control_prefix(value::Field) = _FieldPrefix(value) @@ -586,14 +681,16 @@ _control_subset(value::_NoSubset) = value _control_subset(value::_SubsetSelection) = value _control_subset(::Nothing) = _NoSubset() _control_subset(value::Relation) = _SubsetSelection(value) -_control_gate(value::Union{_NoGate,_ParameterGate,_FieldGate}) = value +_control_gate(value::Union{_NoGate, _ParameterGate, _FieldGate}) = value _control_gate(::Nothing) = _NoGate() _control_gate(value::Parameter) = _ParameterGate(value) _control_gate(value::Field) = _FieldGate(value) Control(; prefix = nothing, mask = nothing, subset = nothing, gate = nothing) = - Control(_control_prefix(prefix), _control_mask(mask), - _control_subset(subset), _control_gate(gate)) + Control( + _control_prefix(prefix), _control_mask(mask), + _control_subset(subset), _control_gate(gate) +) abstract type _PublicationComponentRole end """`PublicationValue(name)` selects a named field from an evaluator result.""" @@ -602,11 +699,13 @@ struct PublicationValue{Name} <: _PublicationComponentRole seal::_StageModelSeal, ::Val{Name} ) where {Name} seal === _STAGE_MODEL_SEAL || error("invalid stage-model seal") - Name isa Symbol || throw(LocalMathValidationError( - "an evaluator value label must be a Symbol"; - stage = :construct, contract = :publication_value_label, - actual = Name, - )) + Name isa Symbol || throw( + LocalMathValidationError( + "an evaluator value label must be a Symbol"; + stage = :construct, contract = :publication_value_label, + actual = Name, + ) + ) return new{Name}() end end @@ -619,20 +718,26 @@ struct Collection{T} ::Type{T}, capacity::Integer; id::UUIDs.UUID = _new_semantic_identity(), ) where {T} - _storage_value_type(T) || throw(LocalMathValidationError( - "a Collection requires an admitted storage value type"; - stage = :construct, contract = :collection_value_type, actual = T, - )) - capacity isa Bool && throw(LocalMathValidationError( - "a Collection capacity must be an integer"; - stage = :construct, contract = :collection_capacity, - actual = capacity, - )) - 0 <= capacity < typemax(Int32) || throw(LocalMathValidationError( - "a Collection capacity must fit below the reserved Int32 terminal"; - stage = :construct, contract = :collection_capacity, - expected = 0:(typemax(Int32) - 1), actual = capacity, - )) + _storage_value_type(T) || throw( + LocalMathValidationError( + "a Collection requires an admitted storage value type"; + stage = :construct, contract = :collection_value_type, actual = T, + ) + ) + capacity isa Bool && throw( + LocalMathValidationError( + "a Collection capacity must be an integer"; + stage = :construct, contract = :collection_capacity, + actual = capacity, + ) + ) + 0 <= capacity < typemax(Int32) || throw( + LocalMathValidationError( + "a Collection capacity must fit below the reserved Int32 terminal"; + stage = :construct, contract = :collection_capacity, + expected = 0:(typemax(Int32) - 1), actual = capacity, + ) + ) return new{T}(Int32(capacity), id) end end @@ -643,38 +748,42 @@ semantic_identity(collection::Collection) = collection.id _control_prefix(value::Collection) = _CollectionCount(value) """`CollectionPublication(collection, role)` publishes a named result to bounded compacted storage.""" -struct CollectionPublication{S<:Collection,Q<:_PublicationComponentRole} +struct CollectionPublication{S <: Collection, Q <: _PublicationComponentRole} collection::S role::Q end """`FoldPublication(role)` connects a named result to an `OrderedFold`.""" -struct FoldPublication{Q<:_PublicationComponentRole} +struct FoldPublication{Q <: _PublicationComponentRole} role::Q end function PublicationValue(label::Symbol) - isempty(String(label)) && throw(LocalMathValidationError( + isempty(String(label)) && throw( + LocalMathValidationError( "an evaluator value label must be nonempty"; stage = :construct, contract = :publication_value_label, - )) + ) + ) return PublicationValue(_STAGE_MODEL_SEAL, Val(label)) end _evaluator_value_name(::PublicationValue{Name}) where {Name} = Name """`FieldPublication(field, relation, role)` routes a named result into a Field.""" -struct FieldPublication{F<:Field,R<:Relation,Q<:_PublicationComponentRole} +struct FieldPublication{F <: Field, R <: Relation, Q <: _PublicationComponentRole} field::F relation::R role::Q function FieldPublication( field::F, relation::R, role::Q - ) where {F<:Field,R<:Relation,Q<:_PublicationComponentRole} - codomain(relation) == field.space || throw(LocalMathValidationError( - "a publication Relation must terminate at its component Field Space"; - stage = :construct, contract = :publication_codomain, - expected = field.space, actual = codomain(relation), - )) - return new{F,R,Q}(field, relation, role) + ) where {F <: Field, R <: Relation, Q <: _PublicationComponentRole} + codomain(relation) == field.space || throw( + LocalMathValidationError( + "a publication Relation must terminate at its component Field Space"; + stage = :construct, contract = :publication_codomain, + expected = field.space, actual = codomain(relation), + ) + ) + return new{F, R, Q}(field, relation, role) end end @@ -691,10 +800,12 @@ struct FillEmpty{T} value::T function FillEmpty(seal::_StageModelSeal, value::T) where {T} seal === _STAGE_MODEL_SEAL || error("invalid stage-model seal") - _storage_value_type(T) || throw(LocalMathValidationError( - "a Unique fill value must use an admitted storage type"; - stage = :construct, contract = :unique_fill_type, actual = T, - )) + _storage_value_type(T) || throw( + LocalMathValidationError( + "a Unique fill value must use an admitted storage type"; + stage = :construct, contract = :unique_fill_type, actual = T, + ) + ) return new{T}(value) end end @@ -709,29 +820,37 @@ struct CollectedValue{T} value::T participates::Bool function CollectedValue(value::T, participates::Bool = true) where {T} - _storage_value_type(T) || throw(LocalMathValidationError( - "a collected value requires an admitted storage type"; - stage = :construct, contract = :collect_result_type, actual = T, - )) + _storage_value_type(T) || throw( + LocalMathValidationError( + "a collected value requires an admitted storage type"; + stage = :construct, contract = :collect_result_type, actual = T, + ) + ) return new{T}(value, participates) end end """`GroupedCollectedValue(group, record, participates=true)` supplies one keyed Collect record.""" -struct GroupedCollectedValue{K,T} +struct GroupedCollectedValue{K, T} key::K value::T participates::Bool function GroupedCollectedValue( key::K, value::T, participates::Bool = true, - ) where {K,T} - K === Int32 || throw(LocalMathValidationError( - "a keyed Collect group must be exactly Int32"; - stage = :construct, contract = :collect_group_key_type, - expected = Int32, actual = K)) - _storage_value_type(T) || throw(LocalMathValidationError( - "a keyed collected value requires an admitted storage type"; - stage = :construct, contract = :collect_result_type, actual = T)) - return new{K,T}(key, value, participates) + ) where {K, T} + K === Int32 || throw( + LocalMathValidationError( + "a keyed Collect group must be exactly Int32"; + stage = :construct, contract = :collect_group_key_type, + expected = Int32, actual = K + ) + ) + _storage_value_type(T) || throw( + LocalMathValidationError( + "a keyed collected value requires an admitted storage type"; + stage = :construct, contract = :collect_result_type, actual = T + ) + ) + return new{K, T}(key, value, participates) end end @@ -740,11 +859,13 @@ struct FoldValue{T} value::T participates::Bool function FoldValue(value::T, participates::Bool = true) where {T} - _storage_value_type(T) || throw(LocalMathValidationError( - "an ordered-fold value requires an admitted storage type"; - stage = :construct, contract = :ordered_fold_result_type, - actual = T, - )) + _storage_value_type(T) || throw( + LocalMathValidationError( + "an ordered-fold value requires an admitted storage type"; + stage = :construct, contract = :ordered_fold_result_type, + actual = T, + ) + ) return new{T}(value, participates) end end @@ -755,7 +876,7 @@ end Declare a bounded compacted sequence. Pass `persistent_source_position()` when a later stage requires retained source positions. """ -struct Collect{T,K,G,O,P,V,E} +struct Collect{T, K, G, O, P, V, E} groups::G order::O projection::P @@ -764,43 +885,57 @@ struct Collect{T,K,G,O,P,V,E} function Collect( seal::_StageModelSeal, ::Type{T}, ::Val{K}, groups::G, order::O, projection::P, overflow::V, onempty::E, - ) where {T,K,G,O,P,V,E} + ) where {T, K, G, O, P, V, E} seal === _STAGE_MODEL_SEAL || error("invalid stage-model seal") - _storage_value_type(T) || throw(LocalMathValidationError( - "Collect values require an admitted storage type"; - stage = :construct, contract = :collect_value_type, actual = T, - )) - 1 <= K <= 32 || throw(LocalMathValidationError( - "Collect emission width must be a reviewed small static bound"; - stage = :construct, contract = :collect_emission_width, - expected = 1:32, actual = K, - )) - groups isa Union{_OneGroup,_GroupBy,_RoutedGroups} || throw(LocalMathValidationError( - "Collect requires one group, value-derived groups, or explicit dense routed groups"; - stage = :construct, contract = :collect_groups, actual = G, - )) - order isa Union{_SourceOrder,_CanonicalBy} || throw( + _storage_value_type(T) || throw( + LocalMathValidationError( + "Collect values require an admitted storage type"; + stage = :construct, contract = :collect_value_type, actual = T, + ) + ) + 1 <= K <= 32 || throw( + LocalMathValidationError( + "Collect emission width must be a reviewed small static bound"; + stage = :construct, contract = :collect_emission_width, + expected = 1:32, actual = K, + ) + ) + groups isa Union{_OneGroup, _GroupBy, _RoutedGroups} || throw( + LocalMathValidationError( + "Collect requires one group, value-derived groups, or explicit dense routed groups"; + stage = :construct, contract = :collect_groups, actual = G, + ) + ) + order isa Union{_SourceOrder, _CanonicalBy} || throw( LocalMathValidationError( "Collect requires source_order or canonical_by semantics"; stage = :construct, contract = :collect_order, actual = O, - )) + ) + ) projection isa Union{ - _NoPersistentProjection,_PersistentSourcePosition} || throw( + _NoPersistentProjection, _PersistentSourcePosition, + } || throw( LocalMathValidationError( "Collect projection is closed to none or source position"; stage = :construct, contract = :collect_projection, actual = P, - )) - overflow isa RejectOverflow || throw(LocalMathValidationError( - "Collect currently requires deterministic overflow rejection"; - stage = :construct, contract = :collect_overflow, actual = V, - )) - onempty isa EmptyCollection || throw(LocalMathValidationError( - "Collect empty input must publish the valid empty sequence"; - stage = :construct, contract = :collect_empty, actual = E, - )) - return new{T,K,G,O,P,V,E}( - groups, order, projection, overflow, onempty) + ) + ) + overflow isa RejectOverflow || throw( + LocalMathValidationError( + "Collect currently requires deterministic overflow rejection"; + stage = :construct, contract = :collect_overflow, actual = V, + ) + ) + onempty isa EmptyCollection || throw( + LocalMathValidationError( + "Collect empty input must publish the valid empty sequence"; + stage = :construct, contract = :collect_empty, actual = E, + ) + ) + return new{T, K, G, O, P, V, E}( + groups, order, projection, overflow, onempty + ) end end @@ -809,62 +944,79 @@ function Collect( order = _SourceOrder(), projection = _NoPersistentProjection(), overflow = RejectOverflow(), onempty = EmptyCollection(), ) where {T} - maximum isa Bool && throw(LocalMathValidationError( - "Collect emission width must be an integer"; - stage = :construct, contract = :collect_emission_width, - actual = maximum, - )) - return Collect(_STAGE_MODEL_SEAL, T, Val(Int(maximum)), groups, - order, projection, overflow, onempty) + maximum isa Bool && throw( + LocalMathValidationError( + "Collect emission width must be an integer"; + stage = :construct, contract = :collect_emission_width, + actual = maximum, + ) + ) + return Collect( + _STAGE_MODEL_SEAL, T, Val(Int(maximum)), groups, + order, projection, overflow, onempty + ) end """`OrderedFold(T, state, transition; order=source_order())` declares a finite ordered recurrence.""" -struct OrderedFold{T,A,F,O} +struct OrderedFold{T, A, F, O} state::A transition::F order::O function OrderedFold( seal::_StageModelSeal, ::Type{T}, state::A, transition::F, order::O, - ) where {T,A,F,O} + ) where {T, A, F, O} seal === _STAGE_MODEL_SEAL || error("invalid stage-model seal") - _storage_value_type(T) || throw(LocalMathValidationError( - "OrderedFold values require an admitted storage type"; - stage = :construct, contract = :ordered_fold_value_type, - actual = T, - )) - state isa InitializedState || throw(LocalMathValidationError( - "OrderedFold requires one typed Field state"; - stage = :construct, contract = :ordered_fold_state, actual = A, - )) - _device_law_callable(transition) || throw(LocalMathValidationError( - "OrderedFold transition must be a concrete device-admissible callable"; - stage = :construct, contract = :ordered_fold_transition, - actual = _device_callable_rejection(transition; - path = (:ordered_fold, :transition)), - )) - order isa Union{_SourceOrder,_CanonicalBy} || throw( + _storage_value_type(T) || throw( + LocalMathValidationError( + "OrderedFold values require an admitted storage type"; + stage = :construct, contract = :ordered_fold_value_type, + actual = T, + ) + ) + state isa InitializedState || throw( + LocalMathValidationError( + "OrderedFold requires one typed Field state"; + stage = :construct, contract = :ordered_fold_state, actual = A, + ) + ) + _device_law_callable(transition) || throw( + LocalMathValidationError( + "OrderedFold transition must be a concrete device-admissible callable"; + stage = :construct, contract = :ordered_fold_transition, + actual = _device_callable_rejection( + transition; + path = (:ordered_fold, :transition) + ), + ) + ) + order isa Union{_SourceOrder, _CanonicalBy} || throw( LocalMathValidationError( "OrderedFold requires source_order or canonical_by semantics"; stage = :construct, contract = :ordered_fold_order, actual = O, - )) - return new{T,A,F,O}(state, transition, order) + ) + ) + return new{T, A, F, O}(state, transition, order) end end -OrderedFold(::Type{T}, state::InitializedState, transition; - order = _SourceOrder()) where {T} = +OrderedFold( + ::Type{T}, state::InitializedState, transition; + order = _SourceOrder() +) where {T} = OrderedFold(_STAGE_MODEL_SEAL, T, state, transition, order) """`UniqueValue(value)` supplies one unconditional Unique value.""" struct UniqueValue{T} value::T function UniqueValue(value::T) where {T} - _storage_value_type(T) || throw(LocalMathValidationError( - "a Unique result value must use an admitted storage type"; - stage = :construct, contract = :unique_result_type, actual = T, - )) + _storage_value_type(T) || throw( + LocalMathValidationError( + "a Unique result value must use an admitted storage type"; + stage = :construct, contract = :unique_result_type, actual = T, + ) + ) return new{T}(value) end end @@ -873,80 +1025,98 @@ struct ConditionalUniqueValue{T} value::T participates::Bool function ConditionalUniqueValue(value::T, participates::Bool) where {T} - _storage_value_type(T) || throw(LocalMathValidationError( - "a Unique result value must use an admitted storage type"; - stage = :construct, contract = :unique_result_type, actual = T, - )) + _storage_value_type(T) || throw( + LocalMathValidationError( + "a Unique result value must use an admitted storage type"; + stage = :construct, contract = :unique_result_type, actual = T, + ) + ) return new{T}(value, participates) end end """`RoutedUniqueValue(key, value)` supplies one runtime-routed Unique value.""" -struct RoutedUniqueValue{K,T} +struct RoutedUniqueValue{K, T} key::K value::T - function RoutedUniqueValue(key::K, value::T) where {K,T} - K in (Int32, UInt32) || throw(LocalMathValidationError( - "a routed Unique key must be Int32 or UInt32"; - stage = :construct, contract = :runtime_relation_key_type, - expected = (Int32, UInt32), actual = K, - )) - _storage_value_type(T) || throw(LocalMathValidationError( - "a routed Unique value requires an admitted storage type"; - stage = :construct, contract = :unique_result_type, actual = T, - )) - return new{K,T}(key, value) + function RoutedUniqueValue(key::K, value::T) where {K, T} + K in (Int32, UInt32) || throw( + LocalMathValidationError( + "a routed Unique key must be Int32 or UInt32"; + stage = :construct, contract = :runtime_relation_key_type, + expected = (Int32, UInt32), actual = K, + ) + ) + _storage_value_type(T) || throw( + LocalMathValidationError( + "a routed Unique value requires an admitted storage type"; + stage = :construct, contract = :unique_result_type, actual = T, + ) + ) + return new{K, T}(key, value) end end """`ConditionalRoutedUniqueValue(key, value, participates)` conditionally supplies a routed Unique value.""" -struct ConditionalRoutedUniqueValue{K,T} +struct ConditionalRoutedUniqueValue{K, T} key::K value::T participates::Bool function ConditionalRoutedUniqueValue( key::K, value::T, participates::Bool, - ) where {K,T} - K in (Int32, UInt32) || throw(LocalMathValidationError( - "a routed Unique key must be Int32 or UInt32"; - stage = :construct, contract = :runtime_relation_key_type, - expected = (Int32, UInt32), actual = K, - )) - _storage_value_type(T) || throw(LocalMathValidationError( - "a routed Unique value requires an admitted storage type"; - stage = :construct, contract = :unique_result_type, actual = T, - )) - return new{K,T}(key, value, participates) + ) where {K, T} + K in (Int32, UInt32) || throw( + LocalMathValidationError( + "a routed Unique key must be Int32 or UInt32"; + stage = :construct, contract = :runtime_relation_key_type, + expected = (Int32, UInt32), actual = K, + ) + ) + _storage_value_type(T) || throw( + LocalMathValidationError( + "a routed Unique value requires an admitted storage type"; + stage = :construct, contract = :unique_result_type, actual = T, + ) + ) + return new{K, T}(key, value, participates) end end """`Unique(T; maximum=1, coverage=TotalCoverage(), onempty=UnreachableEmpty())` rejects conflicts.""" -struct Unique{T,K,C,E} +struct Unique{T, K, C, E} coverage::C onempty::E function Unique( seal::_StageModelSeal, ::Type{T}, ::Val{K}, coverage::C, onempty::E - ) where {T,K,C,E} + ) where {T, K, C, E} seal === _STAGE_MODEL_SEAL || error("invalid stage-model seal") - _storage_value_type(T) || throw(LocalMathValidationError( - "Unique values require an admitted storage value type"; - stage = :construct, contract = :unique_value_type, actual = T, - )) - 1 <= K <= 32 || throw(LocalMathValidationError( - "Unique emission width must be a reviewed small static bound"; - stage = :construct, contract = :unique_emission_width, - expected = 1:32, actual = K, - )) + _storage_value_type(T) || throw( + LocalMathValidationError( + "Unique values require an admitted storage value type"; + stage = :construct, contract = :unique_value_type, actual = T, + ) + ) + 1 <= K <= 32 || throw( + LocalMathValidationError( + "Unique emission width must be a reviewed small static bound"; + stage = :construct, contract = :unique_emission_width, + expected = 1:32, actual = K, + ) + ) valid_empty = coverage isa TotalCoverage ? onempty isa UnreachableEmpty : coverage isa PartialCoverage && - (onempty isa PreserveEmpty || - (onempty isa FillEmpty && typeof(onempty.value) === T)) - valid_empty || throw(LocalMathValidationError( - "Unique coverage and empty behavior are incoherent"; - stage = :construct, contract = :unique_empty_law, - actual = (coverage, onempty), - )) - return new{T,K,C,E}(coverage, onempty) + ( + onempty isa PreserveEmpty || + (onempty isa FillEmpty && typeof(onempty.value) === T) + ) + valid_empty || throw( + LocalMathValidationError( + "Unique coverage and empty behavior are incoherent"; + stage = :construct, contract = :unique_empty_law, + actual = (coverage, onempty), + ) + ) + return new{T, K, C, E}(coverage, onempty) end end @@ -954,16 +1124,20 @@ function Unique( ::Type{T}; maximum::Integer = 1, coverage = TotalCoverage(), onempty = UnreachableEmpty(), ) where {T} - maximum isa Bool && throw(LocalMathValidationError( - "Unique emission width must be an integer"; - stage = :construct, contract = :unique_emission_width, - actual = maximum, - )) - 1 <= maximum <= 32 || throw(LocalMathValidationError( - "Unique emission width must be a reviewed small static bound"; - stage = :construct, contract = :unique_emission_width, - expected = 1:32, actual = maximum, - )) + maximum isa Bool && throw( + LocalMathValidationError( + "Unique emission width must be an integer"; + stage = :construct, contract = :unique_emission_width, + actual = maximum, + ) + ) + 1 <= maximum <= 32 || throw( + LocalMathValidationError( + "Unique emission width must be a reviewed small static bound"; + stage = :construct, contract = :unique_emission_width, + expected = 1:32, actual = maximum, + ) + ) return Unique( _STAGE_MODEL_SEAL, T, Val(Int(maximum)), coverage, onempty ) @@ -974,32 +1148,38 @@ struct Contribution{T} value::T participates::Bool function Contribution(value::T, participates::Bool = true) where {T} - _storage_value_type(T) || throw(LocalMathValidationError( - "a Reduce contribution requires an admitted storage value type"; - stage = :construct, contract = :reduce_result_type, actual = T, - )) + _storage_value_type(T) || throw( + LocalMathValidationError( + "a Reduce contribution requires an admitted storage value type"; + stage = :construct, contract = :reduce_result_type, actual = T, + ) + ) return new{T}(value, participates) end end """`RoutedContribution(key, value, participates=true)` supplies one routed Reduce contribution.""" -struct RoutedContribution{K,T} +struct RoutedContribution{K, T} key::K value::T participates::Bool function RoutedContribution( key::K, value::T, participates::Bool = true, - ) where {K,T} - K in (Int32, UInt32) || throw(LocalMathValidationError( - "a routed Reduce key must be Int32 or UInt32"; - stage = :construct, contract = :runtime_relation_key_type, - expected = (Int32, UInt32), actual = K, - )) - _storage_value_type(T) || throw(LocalMathValidationError( - "a routed Reduce contribution requires an admitted storage type"; - stage = :construct, contract = :reduce_result_type, actual = T, - )) - return new{K,T}(key, value, participates) + ) where {K, T} + K in (Int32, UInt32) || throw( + LocalMathValidationError( + "a routed Reduce key must be Int32 or UInt32"; + stage = :construct, contract = :runtime_relation_key_type, + expected = (Int32, UInt32), actual = K, + ) + ) + _storage_value_type(T) || throw( + LocalMathValidationError( + "a routed Reduce contribution requires an admitted storage type"; + stage = :construct, contract = :reduce_result_type, actual = T, + ) + ) + return new{K, T}(key, value, participates) end end @@ -1007,10 +1187,12 @@ end struct IdentitySeed{T} value::T function IdentitySeed(value::T) where {T} - _storage_value_type(T) || throw(LocalMathValidationError( - "a Reduce identity requires an admitted storage value type"; - stage = :construct, contract = :reduce_seed_type, actual = T, - )) + _storage_value_type(T) || throw( + LocalMathValidationError( + "a Reduce identity requires an admitted storage value type"; + stage = :construct, contract = :reduce_seed_type, actual = T, + ) + ) return new{T}(value) end end @@ -1022,46 +1204,54 @@ struct CanonicalLeftFold end struct RelaxedAtomic end """`Reduce(T, operation; maximum, seed, order)` declares an explicitly initialized fold.""" -struct Reduce{T,K,F,S,O} +struct Reduce{T, K, F, S, O} operation::F seed::S order::O function Reduce( seal::_StageModelSeal, ::Type{T}, ::Val{K}, operation::F, seed::S, order::O, - ) where {T,K,F,S,O} + ) where {T, K, F, S, O} seal === _STAGE_MODEL_SEAL || error("invalid stage-model seal") - _storage_value_type(T) || throw(LocalMathValidationError( - "Reduce values require an admitted storage value type"; - stage = :construct, contract = :reduce_value_type, actual = T, - )) - 1 <= K <= 32 || throw(LocalMathValidationError( - "Reduce emission width must be a reviewed small static bound"; - stage = :construct, contract = :reduce_emission_width, - expected = 1:32, actual = K, - )) + _storage_value_type(T) || throw( + LocalMathValidationError( + "Reduce values require an admitted storage value type"; + stage = :construct, contract = :reduce_value_type, actual = T, + ) + ) + 1 <= K <= 32 || throw( + LocalMathValidationError( + "Reduce emission width must be a reviewed small static bound"; + stage = :construct, contract = :reduce_emission_width, + expected = 1:32, actual = K, + ) + ) seed isa ExistingSeed || (seed isa IdentitySeed && typeof(seed.value) === T) || throw( - LocalMathValidationError( - "Reduce seed must be Existing or an exact-typed identity"; - stage = :construct, contract = :reduce_seed, - expected = T, actual = typeof(seed), - ) + LocalMathValidationError( + "Reduce seed must be Existing or an exact-typed identity"; + stage = :construct, contract = :reduce_seed, + expected = T, actual = typeof(seed), ) - order isa Union{CanonicalLeftFold,RelaxedAtomic} || throw( + ) + order isa Union{CanonicalLeftFold, RelaxedAtomic} || throw( LocalMathValidationError( "Reduce requires an explicit canonical or relaxed order law"; stage = :construct, contract = :reduce_order, actual = typeof(order), ) ) - _device_law_callable(operation) || throw(LocalMathValidationError( - "Reduce operation must be one concrete structurally device-admissible callable"; - stage = :construct, contract = :reduce_operation, - actual = _device_callable_rejection(operation; - path = (:reduce, :operation)), - )) - return new{T,K,F,S,O}(operation, seed, order) + _device_law_callable(operation) || throw( + LocalMathValidationError( + "Reduce operation must be one concrete structurally device-admissible callable"; + stage = :construct, contract = :reduce_operation, + actual = _device_callable_rejection( + operation; + path = (:reduce, :operation) + ), + ) + ) + return new{T, K, F, S, O}(operation, seed, order) end end @@ -1083,14 +1273,14 @@ _resolve_tie_type(::TieMin{I}) where {I} = I _resolve_tie_type(::TieMax{I}) where {I} = I """`ResolutionValue(rank, tie, payload, participates=true)` supplies one Resolve candidate.""" -struct ResolutionValue{R,I,T} +struct ResolutionValue{R, I, T} rank::R tie::I value::T participates::Bool function ResolutionValue( rank::R, tie::I, value::T, participates::Bool = true, - ) where {R,I,T} + ) where {R, I, T} _storage_value_type(R) && _storage_value_type(I) && _storage_value_type(T) || throw( LocalMathValidationError( @@ -1101,21 +1291,21 @@ struct ResolutionValue{R,I,T} ) _qualified_rank_shape(R) && (I === _CanonicalOrdinal || _qualified_rank_shape(I)) || throw( - LocalMathValidationError( - "Resolve rank and explicit tie types require a total qualified shape"; - stage = :construct, contract = :resolve_order_shape, - expected = :qualified_total_order_shape, - actual = (R, I), - ) + LocalMathValidationError( + "Resolve rank and explicit tie types require a total qualified shape"; + stage = :construct, contract = :resolve_order_shape, + expected = :qualified_total_order_shape, + actual = (R, I), ) - return new{R,I,T}(rank, tie, value, participates) + ) + return new{R, I, T}(rank, tie, value, participates) end end -ResolutionValue(rank::R, value::T, participates::Bool = true) where {R,T} = +ResolutionValue(rank::R, value::T, participates::Bool = true) where {R, T} = ResolutionValue(rank, _CanonicalOrdinal(), value, participates) """`RoutedResolutionValue(key, rank, tie, payload, participates=true)` supplies one routed Resolve candidate.""" -struct RoutedResolutionValue{K,R,I,T} +struct RoutedResolutionValue{K, R, I, T} key::K rank::R tie::I @@ -1124,36 +1314,41 @@ struct RoutedResolutionValue{K,R,I,T} function RoutedResolutionValue( key::K, rank::R, tie::I, value::T, participates::Bool = true, - ) where {K,R,I,T} - K in (Int32, UInt32) || throw(LocalMathValidationError( - "a routed Resolve key must be Int32 or UInt32"; - stage = :construct, contract = :runtime_relation_key_type, - expected = (Int32, UInt32), actual = K, - )) + ) where {K, R, I, T} + K in (Int32, UInt32) || throw( + LocalMathValidationError( + "a routed Resolve key must be Int32 or UInt32"; + stage = :construct, contract = :runtime_relation_key_type, + expected = (Int32, UInt32), actual = K, + ) + ) _storage_value_type(R) && _storage_value_type(I) && - _storage_value_type(T) || throw(LocalMathValidationError( + _storage_value_type(T) || throw( + LocalMathValidationError( "a routed Resolve candidate requires admitted rank, tie, and value types"; stage = :construct, contract = :resolve_result_type, actual = (R, I, T), - )) + ) + ) _qualified_rank_shape(R) && (I === _CanonicalOrdinal || _qualified_rank_shape(I)) || throw( - LocalMathValidationError( - "routed Resolve rank and tie require qualified total shapes"; - stage = :construct, contract = :resolve_order_shape, - actual = (R, I), - ) + LocalMathValidationError( + "routed Resolve rank and tie require qualified total shapes"; + stage = :construct, contract = :resolve_order_shape, + actual = (R, I), ) - return new{K,R,I,T}(key, rank, tie, value, participates) + ) + return new{K, R, I, T}(key, rank, tie, value, participates) end end RoutedResolutionValue( - key::K, rank::R, value::T, participates::Bool = true, - ) where {K,R,T} = RoutedResolutionValue( - key, rank, _CanonicalOrdinal(), value, participates) + key::K, rank::R, value::T, participates::Bool = true, +) where {K, R, T} = RoutedResolutionValue( + key, rank, _CanonicalOrdinal(), value, participates +) """`Resolve(rank_type, value_type; maximum, direction, tie, lower, upper, onempty)` selects one bounded candidate.""" -struct Resolve{R,I,T,K,D,L,E} +struct Resolve{R, I, T, K, D, L, E} direction::D tie::L lower::R @@ -1163,7 +1358,7 @@ struct Resolve{R,I,T,K,D,L,E} seal::_StageModelSeal, ::Type{R}, ::Type{I}, ::Type{T}, ::Val{K}, direction::D, tie::L, lower::R, upper::R, onempty::E, - ) where {R,I,T,K,D,L,E} + ) where {R, I, T, K, D, L, E} seal === _STAGE_MODEL_SEAL || error("invalid stage-model seal") _storage_value_type(R) && _storage_value_type(I) && _storage_value_type(T) || throw( @@ -1171,23 +1366,25 @@ struct Resolve{R,I,T,K,D,L,E} "Resolve requires admitted rank and payload types"; stage = :construct, contract = :resolve_value_type, actual = (R, I, T), - ) ) + ) _qualified_rank_shape(R) && (I === _CanonicalOrdinal || _qualified_rank_shape(I)) || throw( - LocalMathValidationError( - "Resolve rank and explicit tie types require a total qualified shape"; - stage = :construct, contract = :resolve_order_shape, - expected = :qualified_total_order_shape, - actual = (R, I), - ) + LocalMathValidationError( + "Resolve rank and explicit tie types require a total qualified shape"; + stage = :construct, contract = :resolve_order_shape, + expected = :qualified_total_order_shape, + actual = (R, I), ) - 1 <= K <= 32 || throw(LocalMathValidationError( - "Resolve emission width must be a reviewed small static bound"; - stage = :construct, contract = :resolve_emission_width, - expected = 1:32, actual = K, - )) - direction isa Union{ArgMin,ArgMax} || throw( + ) + 1 <= K <= 32 || throw( + LocalMathValidationError( + "Resolve emission width must be a reviewed small static bound"; + stage = :construct, contract = :resolve_emission_width, + expected = 1:32, actual = K, + ) + ) + direction isa Union{ArgMin, ArgMax} || throw( LocalMathValidationError( "Resolve requires an explicit ArgMin or ArgMax law"; stage = :construct, contract = :resolve_direction, @@ -1199,21 +1396,23 @@ struct Resolve{R,I,T,K,D,L,E} catch false end - ordered_bounds || throw(LocalMathValidationError( - "Resolve rank bounds must form a closed ordered interval"; - stage = :construct, contract = :resolve_rank_bounds, - expected = :lower_not_greater_than_upper, - actual = (lower, upper), - )) + ordered_bounds || throw( + LocalMathValidationError( + "Resolve rank bounds must form a closed ordered interval"; + stage = :construct, contract = :resolve_rank_bounds, + expected = :lower_not_greater_than_upper, + actual = (lower, upper), + ) + ) onempty isa PreserveEmpty || (onempty isa FillEmpty && typeof(onempty.value) === T) || throw( - LocalMathValidationError( - "Resolve empty behavior must preserve or fill the payload type"; - stage = :construct, contract = :resolve_empty, - expected = T, actual = typeof(onempty), - ) + LocalMathValidationError( + "Resolve empty behavior must preserve or fill the payload type"; + stage = :construct, contract = :resolve_empty, + expected = T, actual = typeof(onempty), ) - return new{R,I,T,K,D,L,E}(direction, tie, lower, upper, onempty) + ) + return new{R, I, T, K, D, L, E}(direction, tie, lower, upper, onempty) end end @@ -1221,26 +1420,33 @@ function Resolve( ::Type{R}, ::Type{T}; maximum::Integer = 1, direction = ArgMin(), tie = CanonicalSourceLaneTie(), lower::R, upper::R, onempty = PreserveEmpty(), - ) where {R,T} - maximum isa Bool && throw(LocalMathValidationError( - "Resolve emission width must be an integer"; - stage = :construct, contract = :resolve_emission_width, - actual = maximum, - )) - 1 <= maximum <= 32 || throw(LocalMathValidationError( - "Resolve emission width must be a reviewed small static bound"; - stage = :construct, contract = :resolve_emission_width, - expected = 1:32, actual = maximum, - )) - tie isa Union{CanonicalSourceLaneTie,TieMin,TieMax} || throw( + ) where {R, T} + maximum isa Bool && throw( LocalMathValidationError( - "Resolve requires a canonical or explicit total tie law"; - stage = :construct, contract = :resolve_tie, - actual = typeof(tie), - )) + "Resolve emission width must be an integer"; + stage = :construct, contract = :resolve_emission_width, + actual = maximum, + ) + ) + 1 <= maximum <= 32 || throw( + LocalMathValidationError( + "Resolve emission width must be a reviewed small static bound"; + stage = :construct, contract = :resolve_emission_width, + expected = 1:32, actual = maximum, + ) + ) + tie isa Union{CanonicalSourceLaneTie, TieMin, TieMax} || throw( + LocalMathValidationError( + "Resolve requires a canonical or explicit total tie law"; + stage = :construct, contract = :resolve_tie, + actual = typeof(tie), + ) + ) I = _resolve_tie_type(tie) - return Resolve(_STAGE_MODEL_SEAL, R, I, T, Val(Int(maximum)), - direction, tie, lower, upper, onempty) + return Resolve( + _STAGE_MODEL_SEAL, R, I, T, Val(Int(maximum)), + direction, tie, lower, upper, onempty + ) end function Reduce( @@ -1249,40 +1455,46 @@ function Reduce( seed, order = CanonicalLeftFold(), ) where {T} - maximum isa Bool && throw(LocalMathValidationError( - "Reduce emission width must be an integer"; - stage = :construct, contract = :reduce_emission_width, - actual = maximum, - )) - 1 <= maximum <= 32 || throw(LocalMathValidationError( - "Reduce emission width must be a reviewed small static bound"; - stage = :construct, contract = :reduce_emission_width, - expected = 1:32, actual = maximum, - )) + maximum isa Bool && throw( + LocalMathValidationError( + "Reduce emission width must be an integer"; + stage = :construct, contract = :reduce_emission_width, + actual = maximum, + ) + ) + 1 <= maximum <= 32 || throw( + LocalMathValidationError( + "Reduce emission width must be a reviewed small static bound"; + stage = :construct, contract = :reduce_emission_width, + expected = 1:32, actual = maximum, + ) + ) return Reduce( _STAGE_MODEL_SEAL, T, Val(Int(maximum)), operation, seed, order, ) end _unique_value_type(::Unique{T}) where {T} = T -_unique_width(::Unique{T,K}) where {T,K} = K +_unique_width(::Unique{T, K}) where {T, K} = K _publication_value_type(::Unique{T}) where {T} = T _publication_value_type(::Reduce{T}) where {T} = T -_publication_value_type(::Resolve{R,I,T}) where {R,I,T} = T +_publication_value_type(::Resolve{R, I, T}) where {R, I, T} = T _publication_value_type(::Collect{T}) where {T} = T _publication_value_type(::OrderedFold{T}) where {T} = T -_publication_width(::Unique{T,K}) where {T,K} = K -_publication_width(::Reduce{T,K}) where {T,K} = K -_publication_width(::Resolve{R,I,T,K}) where {R,I,T,K} = K -_publication_width(::Collect{T,K}) where {T,K} = K +_publication_width(::Unique{T, K}) where {T, K} = K +_publication_width(::Reduce{T, K}) where {T, K} = K +_publication_width(::Resolve{R, I, T, K}) where {R, I, T, K} = K +_publication_width(::Collect{T, K}) where {T, K} = K _publication_width(::OrderedFold) = 1 -_unique_relation_admitted(::Union{ - _IdentityRelation,_AffineRelation,_FixedRelation,_ProductRelation, - _ComposedRelation, - _BoundaryRelation,_MaskedRelation,_SelectedRelation,_InverseRelation, - _PackedRelation,_RuntimeRelation,_FieldIndexRelation, -}) = true +_unique_relation_admitted( + ::Union{ + _IdentityRelation, _AffineRelation, _FixedRelation, _ProductRelation, + _ComposedRelation, + _BoundaryRelation, _MaskedRelation, _SelectedRelation, _InverseRelation, + _PackedRelation, _RuntimeRelation, _FieldIndexRelation, + } +) = true _unique_relation_admitted(_) = false _runtime_relation_key_type(::Relation{<:_RuntimeRelation{K}}) where {K} = K @@ -1291,17 +1503,19 @@ _runtime_relation_key_type(::Relation) = nothing function _validate_publication(components::Tuple, law::Unique) length(components) == 1 && only(components).role isa PublicationValue || throw( - LocalMathValidationError( - "Unique owns exactly one evaluator-fed value component"; - stage = :construct, contract = :unique_components, - ) + LocalMathValidationError( + "Unique owns exactly one evaluator-fed value component"; + stage = :construct, contract = :unique_components, ) + ) _unique_relation_admitted(only(components).relation.representation) || - throw(LocalMathValidationError( + throw( + LocalMathValidationError( "Unique admits only structurally addressed Relations"; stage = :construct, contract = :unique_relation, actual = typeof(only(components).relation.representation), - )) + ) + ) _relation_ghost_space(only(components).relation) === nothing || throw( LocalMathValidationError( "Unique does not publish into a ghost boundary"; @@ -1330,17 +1544,19 @@ end function _validate_publication(components::Tuple, law::Reduce) length(components) == 1 && only(components).role isa PublicationValue || throw( - LocalMathValidationError( - "Reduce owns exactly one evaluator-fed contribution component"; - stage = :construct, contract = :reduce_components, - ) + LocalMathValidationError( + "Reduce owns exactly one evaluator-fed contribution component"; + stage = :construct, contract = :reduce_components, ) + ) _unique_relation_admitted(only(components).relation.representation) || - throw(LocalMathValidationError( + throw( + LocalMathValidationError( "Reduce admits only structurally addressed Relations"; stage = :construct, contract = :reduce_relation, actual = typeof(only(components).relation.representation), - )) + ) + ) _relation_ghost_space(only(components).relation) === nothing || throw( LocalMathValidationError( "Reduce does not publish into a ghost boundary"; @@ -1369,17 +1585,19 @@ end function _validate_publication(components::Tuple, law::Resolve) length(components) == 1 && only(components).role isa PublicationValue || throw( - LocalMathValidationError( - "Resolve owns exactly one evaluator-fed candidate component"; - stage = :construct, contract = :resolve_components, - ) + LocalMathValidationError( + "Resolve owns exactly one evaluator-fed candidate component"; + stage = :construct, contract = :resolve_components, ) + ) _unique_relation_admitted(only(components).relation.representation) || - throw(LocalMathValidationError( + throw( + LocalMathValidationError( "Resolve admits only structurally addressed Relations"; stage = :construct, contract = :resolve_relation, actual = typeof(only(components).relation.representation), - )) + ) + ) _relation_ghost_space(only(components).relation) === nothing || throw( LocalMathValidationError( "Resolve does not publish into a ghost boundary"; @@ -1409,17 +1627,20 @@ function _validate_publication(components::Tuple, law::Collect) length(components) == 1 && only(components) isa CollectionPublication && only(components).role isa PublicationValue || throw( - LocalMathValidationError( - "Collect owns exactly one evaluator-fed Collection component"; - stage = :construct, contract = :collect_components, - )) + LocalMathValidationError( + "Collect owns exactly one evaluator-fed Collection component"; + stage = :construct, contract = :collect_components, + ) + ) eltype(only(components).collection) === _publication_value_type(law) || - throw(LocalMathValidationError( + throw( + LocalMathValidationError( "Collect value type must equal its Collection element type"; stage = :construct, contract = :collect_component_type, expected = _publication_value_type(law), actual = eltype(only(components).collection), - )) + ) + ) return nothing end @@ -1427,79 +1648,112 @@ function _validate_publication(components::Tuple, law::OrderedFold) length(components) == 1 && only(components) isa FoldPublication && only(components).role isa PublicationValue || throw( - LocalMathValidationError( - "OrderedFold owns exactly one evaluator-fed recurrence component"; - stage = :construct, contract = :ordered_fold_components, - )) + LocalMathValidationError( + "OrderedFold owns exactly one evaluator-fed recurrence component"; + stage = :construct, contract = :ordered_fold_components, + ) + ) return nothing end function _validate_publication(components::Tuple, law) - throw(LocalMathValidationError( - "Publication uses an unsupported mathematical law"; - stage = :construct, contract = :publication_law, - actual = typeof(law), - )) + throw( + LocalMathValidationError( + "Publication uses an unsupported mathematical law"; + stage = :construct, contract = :publication_law, + actual = typeof(law), + ) + ) end """`Publication(components, law, origin)` attaches destinations to one publication law.""" -struct Publication{C<:Tuple,L} +struct Publication{C <: Tuple, L} components::C law::L origin::SourceOrigin function Publication( components::C, law::L, origin::SourceOrigin = _NO_SOURCE_ORIGIN, - ) where {C<:Tuple,L} - isempty(components) && throw(LocalMathValidationError( - "a Publication requires at least one component"; - stage = :construct, contract = :publication_components, - )) - all(component -> component isa Union{ - FieldPublication,CollectionPublication, - FoldPublication}, components) || - throw(LocalMathValidationError( + ) where {C <: Tuple, L} + isempty(components) && throw( + LocalMathValidationError( + "a Publication requires at least one component"; + stage = :construct, contract = :publication_components, + ) + ) + all( + component -> component isa Union{ + FieldPublication, CollectionPublication, + FoldPublication, + }, components + ) || + throw( + LocalMathValidationError( "Publication components must use the closed component descriptor"; stage = :construct, contract = :publication_components, - )) + ) + ) _validate_publication(components, law) - return new{C,L}(components, law, origin) + return new{C, L}(components, law, origin) end end -Publication(field::Field, relation::Relation, law; - value::Symbol = :value, origin::SourceOrigin = _NO_SOURCE_ORIGIN) = - Publication((FieldPublication( - field, relation, PublicationValue(value)),), law, origin) - -Publication(collection::Collection, law; - value::Symbol = :value, origin::SourceOrigin = _NO_SOURCE_ORIGIN) = - Publication((CollectionPublication( - collection, PublicationValue(value)),), law, origin) - -Publication(law::OrderedFold; - value::Symbol = :value, origin::SourceOrigin = _NO_SOURCE_ORIGIN) = - Publication((FoldPublication( - PublicationValue(value)),), law, origin) +Publication( + field::Field, relation::Relation, law; + value::Symbol = :value, origin::SourceOrigin = _NO_SOURCE_ORIGIN +) = + Publication( + ( + FieldPublication( + field, relation, PublicationValue(value) + ), + ), law, origin +) + +Publication( + collection::Collection, law; + value::Symbol = :value, origin::SourceOrigin = _NO_SOURCE_ORIGIN +) = + Publication( + ( + CollectionPublication( + collection, PublicationValue(value) + ), + ), law, origin +) + +Publication( + law::OrderedFold; + value::Symbol = :value, origin::SourceOrigin = _NO_SOURCE_ORIGIN +) = + Publication( + ( + FoldPublication( + PublicationValue(value) + ), + ), law, origin +) function _evaluator_port_names(publications::Tuple) - return Tuple(_evaluator_value_name(component.role) - for publication in publications - for component in publication.components - if component.role isa PublicationValue) + return Tuple( + _evaluator_value_name(component.role) + for publication in publications + for component in publication.components + if component.role isa PublicationValue + ) end function _validate_stage_publication_fields(publications::Tuple) spatial = Tuple( semantic_identity(component.field) - for publication in publications - for component in publication.components - if component isa FieldPublication + for publication in publications + for component in publication.components + if component isa FieldPublication ) folds = Tuple( semantic_identity(component.target) - for publication in publications - if publication.law isa OrderedFold - for component in values(publication.law.state.components) + for publication in publications + if publication.law isa OrderedFold + for component in values(publication.law.state.components) ) identities = (spatial..., folds...) length(unique(identities)) == length(identities) || throw( @@ -1518,9 +1772,9 @@ end function _validate_stage_collection_uniqueness(publications::Tuple) identities = Tuple( semantic_identity(component.collection) - for publication in publications - for component in publication.components - if component isa CollectionPublication + for publication in publications + for component in publication.components + if component isa CollectionPublication ) length(unique(identities)) == length(identities) || throw( LocalMathValidationError( @@ -1528,19 +1782,24 @@ function _validate_stage_collection_uniqueness(publications::Tuple) stage = :construct, contract = :stage_collection_uniqueness, actual = identities, - )) + ) + ) return nothing end function _validate_stage_publication_domain( - publication::Publication, source::Space) + publication::Publication, source::Space + ) for component in publication.components component isa Union{ - CollectionPublication,FoldPublication} && continue - domain(component.relation) == source || throw(LocalMathValidationError( - "every spatial Publication Relation must originate at the stage source"; - stage = :construct, contract = :stage_publication_domain, - )) + CollectionPublication, FoldPublication, + } && continue + domain(component.relation) == source || throw( + LocalMathValidationError( + "every spatial Publication Relation must originate at the stage source"; + stage = :construct, contract = :stage_publication_domain, + ) + ) end if publication.law isa Collect width = _publication_width(publication.law) @@ -1549,34 +1808,46 @@ function _validate_stage_publication_domain( "Collect source/lane ordinals must fit below the reserved Int32 terminal"; stage = :construct, contract = :collect_candidate_ordinal, expected = :nonterminal_int32, actual = (length(source), width), - )) + ) + ) end return nothing end function _validate_ordered_fold_stage_boundary( - publications::Tuple, accesses::NamedTuple, control::Control) - position = findfirst(publication -> publication.law isa OrderedFold, - publications) + publications::Tuple, accesses::NamedTuple, control::Control + ) + position = findfirst( + publication -> publication.law isa OrderedFold, + publications + ) position === nothing && return nothing - length(publications) == 1 || throw(LocalMathValidationError( - "OrderedFold must be the sole terminal publication of its Stage"; - stage = :construct, contract = :ordered_fold_terminal_publication, - expected = 1, actual = length(publications), - )) + length(publications) == 1 || throw( + LocalMathValidationError( + "OrderedFold must be the sole terminal publication of its Stage"; + stage = :construct, contract = :ordered_fold_terminal_publication, + expected = 1, actual = length(publications), + ) + ) law = publications[position].law - targets = Set(semantic_identity(component.target) - for component in values(law.state.components)) - copied_sources = Set(semantic_identity(component.source) - for component in values(law.state.components) - if component.source isa Field) + targets = Set( + semantic_identity(component.target) + for component in values(law.state.components) + ) + copied_sources = Set( + semantic_identity(component.source) + for component in values(law.state.components) + if component.source isa Field + ) read_fields = Set(semantic_identity(access.field) for access in values(accesses)) - isempty(intersect(targets, read_fields)) || throw(LocalMathValidationError( - "OrderedFold targets cannot be ordinary Stage reads"; - stage = :construct, contract = :ordered_fold_target_access_alias, - actual = intersect(targets, read_fields), - )) + isempty(intersect(targets, read_fields)) || throw( + LocalMathValidationError( + "OrderedFold targets cannot be ordinary Stage reads"; + stage = :construct, contract = :ordered_fold_target_access_alias, + actual = intersect(targets, read_fields), + ) + ) control_fields = UUIDs.UUID[] control.prefix isa _FieldPrefix && push!(control_fields, semantic_identity(control.prefix.field)) @@ -1598,14 +1869,16 @@ function _validate_ordered_fold_stage_boundary( stage = :construct, contract = :ordered_fold_target_control_alias, actual = intersect(targets, Set(control_fields)), - )) + ) + ) isempty(intersect(copied_sources, Set(control_fields))) || throw( LocalMathValidationError( "OrderedFold copied sources cannot govern the same Stage control"; stage = :construct, contract = :ordered_fold_source_control_alias, actual = intersect(copied_sources, Set(control_fields)), - )) + ) + ) return nothing end @@ -1613,12 +1886,12 @@ function _unique_lane_type_valid(lane::Type, publication::Publication) expected_value = _unique_value_type(publication.law) key_type = _runtime_relation_key_type(only(publication.components).relation) if key_type === nothing - lane <: Union{UniqueValue,ConditionalUniqueValue} || return false + lane <: Union{UniqueValue, ConditionalUniqueValue} || return false lane.parameters[1] === expected_value || return false publication.law.coverage isa TotalCoverage && lane <: ConditionalUniqueValue && return false else - lane <: Union{RoutedUniqueValue,ConditionalRoutedUniqueValue} || + lane <: Union{RoutedUniqueValue, ConditionalRoutedUniqueValue} || return false lane.parameters[1] === key_type && lane.parameters[2] === expected_value || return false @@ -1667,15 +1940,15 @@ function _ordered_fold_lane_type_valid(lane::Type, publication::Publication) return lane.parameters[1] === _publication_value_type(publication.law) end -_publication_lane_type_valid(lane::Type, publication::Publication{C,<:Unique}) where {C} = +_publication_lane_type_valid(lane::Type, publication::Publication{C, <:Unique}) where {C} = _unique_lane_type_valid(lane, publication) -_publication_lane_type_valid(lane::Type, publication::Publication{C,<:Reduce}) where {C} = +_publication_lane_type_valid(lane::Type, publication::Publication{C, <:Reduce}) where {C} = _reduce_lane_type_valid(lane, publication) -_publication_lane_type_valid(lane::Type, publication::Publication{C,<:Resolve}) where {C} = +_publication_lane_type_valid(lane::Type, publication::Publication{C, <:Resolve}) where {C} = _resolve_lane_type_valid(lane, publication) -_publication_lane_type_valid(lane::Type, publication::Publication{C,<:Collect}) where {C} = +_publication_lane_type_valid(lane::Type, publication::Publication{C, <:Collect}) where {C} = _collect_lane_type_valid(lane, publication) -_publication_lane_type_valid(lane::Type, publication::Publication{C,<:OrderedFold}) where {C} = +_publication_lane_type_valid(lane::Type, publication::Publication{C, <:OrderedFold}) where {C} = _ordered_fold_lane_type_valid(lane, publication) function _validate_evaluator_result_type(publications::Tuple, result_type) @@ -1687,43 +1960,53 @@ function _validate_evaluator_result_type(publications::Tuple, result_type) ) ) names = _evaluator_port_names(publications) - result_type.parameters[1] == names || throw(LocalMathValidationError( - "evaluator result labels and order must exactly match publications"; - stage = :construct, contract = :evaluator_result_ports, - expected = names, actual = result_type.parameters[1], - hint = "return a NamedTuple whose fields match the publication roles in authored order", - )) + result_type.parameters[1] == names || throw( + LocalMathValidationError( + "evaluator result labels and order must exactly match publications"; + stage = :construct, contract = :evaluator_result_ports, + expected = names, actual = result_type.parameters[1], + hint = "return a NamedTuple whose fields match the publication roles in authored order", + ) + ) result_types = result_type.parameters[2].parameters for (publication, port_type) in zip(publications, result_types) - port = only(_evaluator_value_name(component.role) - for component in publication.components - if component.role isa PublicationValue) + port = only( + _evaluator_value_name(component.role) + for component in publication.components + if component.role isa PublicationValue + ) width = _publication_width(publication.law) lanes = if width == 1 (port_type,) elseif port_type <: Tuple && length(port_type.parameters) == width port_type.parameters else - throw(LocalMathValidationError( - "evaluator result has the wrong fixed emission width"; - stage = :construct, contract = :evaluator_result_width, - port, origin = publication.origin, - expected = (width, law = typeof(publication.law)), - actual = (inferred_result_type = port_type,), - )) - end - all(lane -> lane isa Type && isconcretetype(lane) && - _publication_lane_type_valid(lane, publication), lanes) || throw( + throw( LocalMathValidationError( - "evaluator result has an invalid publication carrier type"; - stage = :construct, contract = :evaluator_result_lane, + "evaluator result has the wrong fixed emission width"; + stage = :construct, contract = :evaluator_result_width, port, origin = publication.origin, - expected = (law = typeof(publication.law), - value_type = _publication_value_type(publication.law)), + expected = (width, law = typeof(publication.law)), actual = (inferred_result_type = port_type,), - hint = "return the carrier required by this publication law and value type", ) ) + end + all( + lane -> lane isa Type && isconcretetype(lane) && + _publication_lane_type_valid(lane, publication), lanes + ) || throw( + LocalMathValidationError( + "evaluator result has an invalid publication carrier type"; + stage = :construct, contract = :evaluator_result_lane, + port, origin = publication.origin, + expected = ( + law = typeof(publication.law), + value_type = _publication_value_type(publication.law), + ), + actual = (inferred_result_type = port_type,), + hint = "return the carrier required by this publication law and value type", + ) + ) end return nothing end @@ -1733,12 +2016,12 @@ function _validate_stage_control(control::Control, source::Space, spec::Evaluato declaration = control.prefix.parameter _parameter_type(declaration) <: Integer && _parameter_type(declaration) !== Bool || throw( - LocalMathValidationError( - "a parameter prefix requires a non-Bool integer parameter"; - stage = :construct, contract = :parameter_prefix, - actual = _parameter_type(declaration), - ) + LocalMathValidationError( + "a parameter prefix requires a non-Bool integer parameter"; + stage = :construct, contract = :parameter_prefix, + actual = _parameter_type(declaration), ) + ) end if control.prefix isa _CollectionCount Int(control.prefix.collection.capacity) <= length(source) || throw( @@ -1751,10 +2034,12 @@ function _validate_stage_control(control::Control, source::Space, spec::Evaluato ) end if control.mask isa _MaskSelection - control.mask.field.space == source || throw(LocalMathValidationError( - "a mask Field must belong to the stage source Space"; - stage = :construct, contract = :mask_source, - )) + control.mask.field.space == source || throw( + LocalMathValidationError( + "a mask Field must belong to the stage source Space"; + stage = :construct, contract = :mask_source, + ) + ) end if control.subset isa _SubsetSelection relation = control.subset.relation @@ -1776,7 +2061,7 @@ end Declare one finite local calculation. Pass `SourceOrigin(source, line; label=nothing)` when provenance is available. """ -struct Stage{S<:Space,A,P,E<:Evaluator,C<:Control,O} +struct Stage{S <: Space, A, P, E <: Evaluator, C <: Control, O} source::S accesses::A publications::P @@ -1786,12 +2071,14 @@ struct Stage{S<:Space,A,P,E<:Evaluator,C<:Control,O} function Stage( source::S, accesses::A, publications::P, evaluator::E, control::C, origin::O, - ) where {S<:Space,A,P,E<:Evaluator,C<:Control,O} - accesses isa NamedTuple || throw(LocalMathValidationError( - "Stage accesses must be an evaluator-role NamedTuple"; - stage = :construct, contract = :stage_accesses, actual = A, - )) - all(access -> access isa Union{Access,CollectionAccess}, values(accesses)) || throw( + ) where {S <: Space, A, P, E <: Evaluator, C <: Control, O} + accesses isa NamedTuple || throw( + LocalMathValidationError( + "Stage accesses must be an evaluator-role NamedTuple"; + stage = :construct, contract = :stage_accesses, actual = A, + ) + ) + all(access -> access isa Union{Access, CollectionAccess}, values(accesses)) || throw( LocalMathValidationError( "Stage accesses must contain only Field or Collection Access descriptors"; stage = :construct, contract = :stage_accesses, @@ -1804,12 +2091,16 @@ struct Stage{S<:Space,A,P,E<:Evaluator,C<:Control,O} actual = keys(accesses), ) ) - all(access -> !(access isa Access) || domain(access.relation) == source, - values(accesses)) || - throw(LocalMathValidationError( + all( + access -> !(access isa Access) || domain(access.relation) == source, + values(accesses) + ) || + throw( + LocalMathValidationError( "every Access Relation must originate at the stage source"; stage = :construct, contract = :stage_access_domain, - )) + ) + ) publications isa Tuple && !isempty(publications) || throw( LocalMathValidationError( "Stage publications must be a nonempty tuple"; @@ -1822,8 +2113,11 @@ struct Stage{S<:Space,A,P,E<:Evaluator,C<:Control,O} stage = :construct, contract = :stage_publications, ) ) - foreach(publication -> _validate_stage_publication_domain( - publication, source), publications) + foreach( + publication -> _validate_stage_publication_domain( + publication, source + ), publications + ) _validate_stage_publication_fields(publications) _validate_stage_collection_uniqueness(publications) _validate_ordered_fold_stage_boundary(publications, accesses, control) @@ -1835,21 +2129,27 @@ struct Stage{S<:Space,A,P,E<:Evaluator,C<:Control,O} actual = labels, ) ) - origin isa SourceOrigin || throw(LocalMathValidationError( - "a Stage origin must be SourceOrigin"; - stage = :construct, contract = :stage_origin, actual = O, - )) + origin isa SourceOrigin || throw( + LocalMathValidationError( + "a Stage origin must be SourceOrigin"; + stage = :construct, contract = :stage_origin, actual = O, + ) + ) _validate_stage_control(control, source, evaluator) - return new{S,A,P,E,C,O}( + return new{S, A, P, E, C, O}( source, accesses, publications, evaluator, control, origin ) end end -function Stage(source::Space, accesses::NamedTuple, publications::Tuple, +function Stage( + source::Space, accesses::NamedTuple, publications::Tuple, evaluator; parameters = ParameterSchema(), control = Control(), - origin::SourceOrigin = _NO_SOURCE_ORIGIN) + origin::SourceOrigin = _NO_SOURCE_ORIGIN + ) schema = parameters isa ParameterSchema ? parameters : ParameterSchema(parameters) - return Stage(source, accesses, publications, - Evaluator(evaluator, schema.declarations), control, origin) + return Stage( + source, accesses, publications, + Evaluator(evaluator, schema.declarations), control, origin + ) end diff --git a/test/fixtures/product_publication_contracts.jl b/test/fixtures/product_publication_contracts.jl new file mode 100644 index 0000000..98c3cf1 --- /dev/null +++ b/test/fixtures/product_publication_contracts.jl @@ -0,0 +1,80 @@ +using Test +import LocalMath +import StaticArrays: SVector, MVector + +struct RoutedProductValue{Resolve} end + +@inline _routed_product(item::Int32) = + (active = isodd(item), count = item, polarity = SVector(Float32(item), -Float32(item))) + +@inline function (::RoutedProductValue{Resolve})(item::Int32, reads, parameters) where {Resolve} + value = _routed_product(item) + return Resolve ? + (value = LocalMath.RoutedResolutionValue(item <= 2 ? Int32(1) : Int32(2), Int32(4) - item, value),) : + (value = LocalMath.RoutedUniqueValue(Int32(4) - item, value),) +end + +function test_routed_product_publication(backend) + initial = _routed_product(Int32(0)) + for resolve in (false, true) + source, destination = LocalMath.Space(3), LocalMath.Space(3) + output = LocalMath.Field(destination, typeof(initial)) + relation = LocalMath.RuntimeRelation(source => destination; degree_bound = 1, key_type = Int32) + publication = resolve ? + LocalMath.Resolve(Int32, typeof(initial); lower = Int32(1), upper = Int32(3)) : + LocalMath.Unique(typeof(initial)) + stage = LocalMath.Stage( + source, NamedTuple(), + (LocalMath.Publication((LocalMath.FieldPublication(output, relation, LocalMath.PublicationValue(:value)),), publication),), + LocalMath.Evaluator(RoutedProductValue{resolve}()), LocalMath.Control(), + LocalMath.SourceOrigin(:routed_product_publication, 1) + ) + prepared = LocalMath.prepare(LocalMath.LocalLaw(stage), output => LocalMath.Allocate(fill(initial, 3)); backend) + wait(LocalMath.execute!(prepared)) + expected = resolve ? Int32[2, 3, 0] : Int32[3, 2, 1] + @test Array(LocalMath.storage(prepared, output)) == _routed_product.(expected) + end + return +end + +struct ProductCopyValue end +@inline (::ProductCopyValue)(item::Int32, reads, parameters) = + (value = LocalMath.UniqueValue(something(reads[1][1].value)),) + +function _prepare_product_copy(backend, value) + space = LocalMath.Space(1) + input = LocalMath.Field(space, typeof(value)) + output = LocalMath.Field(space, typeof(value)) + relation = LocalMath.IdentityRelation(space) + stage = LocalMath.Stage( + space, (value = LocalMath.Access(input, relation; required = true),), + (LocalMath.Publication((LocalMath.FieldPublication(output, relation, LocalMath.PublicationValue(:value)),), LocalMath.Unique(typeof(value))),), + LocalMath.Evaluator(ProductCopyValue()), LocalMath.Control(), LocalMath.SourceOrigin(:product_layout, 1) + ) + return LocalMath.prepare(LocalMath.LocalLaw(stage), input => LocalMath.Allocate(fill(value, 1)), output => LocalMath.Allocate(undef); backend) +end + +function test_product_layout_rejections( + backend, unsupported; + unsupported_error = LocalMath.LocalMathValidationError, + unsupported_message = string(typeof(unsupported)) + ) + oversized = (values = ntuple(_ -> 1.0f0, 17),) + @test_throws LocalMath.LocalMathValidationError _prepare_product_copy(backend, oversized) + failure = try + _prepare_product_copy(backend, (inner = (unsupported = unsupported,),)) + nothing + catch error + error + end + @test failure isa unsupported_error + @test occursin(unsupported_message, failure === nothing ? "" : sprint(showerror, failure)) + for value in ((inner = (pointer = Ptr{Float32}(0),),), (inner = (name = :metadata,),), (inner = MVector(1.0f0, 2.0f0),)) + @test_throws LocalMath.LocalMathValidationError LocalMath.Field(LocalMath.Space(1), typeof(value)) + capture = let captured = value + (item, reads, parameters) -> (value = LocalMath.UniqueValue(captured),) + end + @test_throws LocalMath.LocalMathValidationError LocalMath.Evaluator(capture) + end + return +end diff --git a/test/metal/product_values.jl b/test/metal/product_values.jl new file mode 100644 index 0000000..35bc2b7 --- /dev/null +++ b/test/metal/product_values.jl @@ -0,0 +1,11 @@ +include(joinpath(@__DIR__, "..", "test_product_values.jl")) + +@testset "named product publication on Metal" begin + test_product_value_publication(Metal.MetalBackend()) + test_routed_product_publication(Metal.MetalBackend()) + test_product_layout_rejections( + Metal.MetalBackend(), 1.0; + unsupported_error = ErrorException, + unsupported_message = "Metal does not support Float64" + ) +end diff --git a/test/metal/runtests.jl b/test/metal/runtests.jl index e7f011a..0e12195 100644 --- a/test/metal/runtests.jl +++ b/test/metal/runtests.jl @@ -13,13 +13,16 @@ const LOCALMATH_METAL_WITNESSES = ( "localmath_authoring.jl", "localmath_correctness.jl", "destination_grouping.jl", + "product_values.jl", ) @testset "LocalMath Metal runner inventory" begin - discovered = Set(filter( - name -> endswith(name, ".jl") && name != "runtests.jl", - readdir(@__DIR__), - )) + discovered = Set( + filter( + name -> endswith(name, ".jl") && name != "runtests.jl", + readdir(@__DIR__), + ) + ) @test discovered == Set(LOCALMATH_METAL_WITNESSES) end @@ -49,13 +52,16 @@ end zbuffer = run_localmath_zbuffer_witness(Metal.MtlArray; backend) dem = run_localmath_compacted_dem_contacts_witness(Metal.MtlArray; backend) active_fem = run_localmath_compacted_active_fem_witness( - Metal.MtlArray; backend) + Metal.MtlArray; backend + ) particle_cells = run_localmath_compacted_particle_cells_witness( - Metal.MtlArray; backend) + Metal.MtlArray; backend + ) rsa = run_localmath_ordered_rsa_witness(Metal.MtlArray; backend) pgs = run_localmath_ordered_pgs_3d_witness(Metal.MtlArray; backend) chemistry = run_localmath_ordered_stoichiometry_witness( - Metal.MtlArray; backend) + Metal.MtlArray; backend + ) authored = run_localmath_authored_domain_witness(Metal.MtlArray; backend) @test lbm.result == lbm.reference diff --git a/test/runtests.jl b/test/runtests.jl index 467e72e..f4d48f5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,6 +10,7 @@ const LOCALMATH_INCLUDED_TESTS = ( "test_stage_planning.jl", "test_stage_preparation.jl", "test_direct_pointwise_stage.jl", + "test_product_values.jl", "test_unique_stage.jl", "test_stage_program_lifecycle.jl", "test_execution_receipts.jl", @@ -91,17 +92,21 @@ const LOCALMATH_TEST_SUITE = Dict{String, Expr}( all_qualified_accesses_are_public = (; ignore = qualified_internal_boundary), ) - @test isempty(Test.detect_ambiguities( - LocalMath, Base; recursive = true - )) + @test isempty( + Test.detect_ambiguities( + LocalMath, Base; recursive = true + ) + ) end end, "test_runner_inventory" => quote @testset "LocalMath test runner inventory" begin - discovered = sort(filter( - name -> startswith(name, "test_") && endswith(name, ".jl"), - readdir(@__DIR__), - )) + discovered = sort( + filter( + name -> startswith(name, "test_") && endswith(name, ".jl"), + readdir(@__DIR__), + ) + ) @test sort(collect($LOCALMATH_INCLUDED_TESTS)) == discovered end end, diff --git a/test/test_product_values.jl b/test/test_product_values.jl new file mode 100644 index 0000000..e6f5065 --- /dev/null +++ b/test/test_product_values.jl @@ -0,0 +1,58 @@ +using Test +import LocalMath +import KernelAbstractions +import StaticArrays: SVector + +include(joinpath(@__DIR__, "fixtures", "product_publication_contracts.jl")) + +struct ProductValueUpdate{P} + increment::P +end + +@inline function (operation::ProductValueUpdate)(item::Int32, reads, parameters) + before = something(reads[1][1].value) + increment = operation.increment + after = ( + active = !before.active, + count = before.count + increment.count, + polarity = before.polarity + increment.polarity, + ) + return (value = LocalMath.UniqueValue(after),) +end + +function test_product_value_publication(backend) + initial = (active = false, count = Int32(2), polarity = SVector(1.0f0, 2.0f0)) + increment = (count = Int32(3), polarity = SVector(0.5f0, 1.0f0)) + space = LocalMath.Space(3) + input = LocalMath.Field(space, typeof(initial)) + output = LocalMath.Field(space, typeof(initial)) + relation = LocalMath.IdentityRelation(space) + stage = LocalMath.Stage( + space, (value = LocalMath.Access(input, relation; required = true),), + (LocalMath.Publication((LocalMath.FieldPublication(output, relation, LocalMath.PublicationValue(:value)),), LocalMath.Unique(typeof(initial))),), + LocalMath.Evaluator(ProductValueUpdate(increment)), LocalMath.Control(), + LocalMath.SourceOrigin(:product_value_publication, 1), + ) + prepared = LocalMath.prepare( + LocalMath.LocalLaw(stage), + input => LocalMath.Allocate(fill(initial, 3)), output => LocalMath.Allocate(undef); backend + ) + wait(LocalMath.execute!(prepared)) + values = Array(LocalMath.storage(prepared, output)) + @test values == fill((active = true, count = Int32(5), polarity = SVector(1.5f0, 3.0f0)), 3) + @test eltype(values) === typeof(initial) + return @test Array(LocalMath.storage(prepared, input)) == fill(initial, 3) +end + +@testset "named products share ordinary field publication" begin + test_product_value_publication(KernelAbstractions.CPU()) + for value in ((name = :metadata,), (values = Float32[1],), (pointer = Ptr{Float32}(0),)) + @test_throws LocalMath.LocalMathValidationError LocalMath.Field(LocalMath.Space(1), typeof(value)) + @test_throws LocalMath.LocalMathValidationError LocalMath.Evaluator(ProductValueUpdate(value)) + end +end + +@testset "routed products and nested layout contracts" begin + test_routed_product_publication(KernelAbstractions.CPU()) + test_product_layout_rejections(KernelAbstractions.CPU(), Int64(1)) +end diff --git a/test/test_spatial_model.jl b/test/test_spatial_model.jl index 06e814e..356b096 100644 --- a/test/test_spatial_model.jl +++ b/test/test_spatial_model.jl @@ -46,7 +46,7 @@ nodes, Base.RefValue{Int32} ) @test_throws LocalMath.LocalMathValidationError LocalMath.Field( - nodes, NamedTuple{(:label,),Tuple{Int32}} + nodes, NamedTuple{(:label,), Tuple{Symbol}} ) @test_throws LocalMath.LocalMathValidationError LocalMath.Field( nodes, Val{1} @@ -93,10 +93,11 @@ schema_epoch = 3, ) scalar_keys = LocalMath.Field(edges, Int32) - tuple_keys = LocalMath.Field(edges, Tuple{Int32,UInt32}) + tuple_keys = LocalMath.Field(edges, Tuple{Int32, UInt32}) indexed = LocalMath.IndexRelation(scalar_keys => nodes) optional_indexed = LocalMath.IndexRelation( - tuple_keys => nodes; optional = true) + tuple_keys => nodes; optional = true + ) masked = LocalMath.MaskedRelation(identity, mask) selected_space = LocalMath.Space(TestEdge, 3) injection = LocalMath.FixedRelation( @@ -145,11 +146,13 @@ ) @test typeof(fixed) === typeof(fixed_other) @test typeof(packed) === typeof(packed_other) - @test typeof(runtime) === typeof(LocalMath.RuntimeRelation( - edges => nodes; - degree_bound = 9, key_type = UInt32, - schema_epoch = 99, - )) + @test typeof(runtime) === typeof( + LocalMath.RuntimeRelation( + edges => nodes; + degree_bound = 9, key_type = UInt32, + schema_epoch = 99, + ) + ) # Structural validation is now the sole package-owned proof minter; # callers still cannot construct a proof or validated evidence directly. @@ -174,7 +177,8 @@ degree_bound = 1, key_type = Vector{Int32}, ) @test_throws LocalMath.LocalMathValidationError LocalMath.IndexRelation( - LocalMath.Field(edges, Float32) => nodes) + LocalMath.Field(edges, Float32) => nodes + ) @test_throws LocalMath.LocalMathValidationError LocalMath.PackedRelation( nodes => edges; degree_bound = 2, capacity = 5, layout = :compressed_offsets, diff --git a/test/test_stage_model.jl b/test/test_stage_model.jl index c75e4fd..4bdeebd 100644 --- a/test/test_stage_model.jl +++ b/test/test_stage_model.jl @@ -79,19 +79,25 @@ struct SMForeignBounds <: LMM._ParameterBounds end SMScaleEvaluator(values_field) ) @test_throws LMM.LocalMathValidationError LMM.Evaluator( - SMArrayCaptureEvaluator(Float32[1, 2])) + SMArrayCaptureEvaluator(Float32[1, 2]) + ) @test_throws LMM.LocalMathValidationError LMM.Evaluator( - SMMutableCaptureEvaluator(Int32(1))) + SMMutableCaptureEvaluator(Int32(1)) + ) unique = LMM.Unique(Float32) publication = LMM.Publication( - values_field, identity, unique; value = :value) + values_field, identity, unique; value = :value + ) component = publication.components[1] control = LMM.Control( - prefix = count, mask = mask_field, subset = identity, gate = enabled) - stage = LMM.Stage(nodes, (value = access,), (publication,), + prefix = count, mask = mask_field, subset = identity, gate = enabled + ) + stage = LMM.Stage( + nodes, (value = access,), (publication,), SMIdentityEvaluator(); parameters = (count, enabled), control, - origin = LMM.SourceOrigin(:stage_model_test, 1)) + origin = LMM.SourceOrigin(:stage_model_test, 1) + ) @test stage.source === nodes @test stage.publications == (publication,) @test stage.control === control @@ -122,20 +128,20 @@ struct SMForeignBounds <: LMM._ParameterBounds end (kind = :obsolete_shape,), :freshness, ) - valid_result = NamedTuple{(:value,),Tuple{LMM.UniqueValue{Float32}}} + valid_result = NamedTuple{(:value,), Tuple{LMM.UniqueValue{Float32}}} @test LMM._validate_evaluator_result_type((publication,), valid_result) === nothing @test_throws LMM.LocalMathValidationError LMM._validate_evaluator_result_type( (publication,), Float32 ) @test_throws LMM.LocalMathValidationError LMM._validate_evaluator_result_type( - (publication,), NamedTuple{(:wrong,),Tuple{LMM.UniqueValue{Float32}}} + (publication,), NamedTuple{(:wrong,), Tuple{LMM.UniqueValue{Float32}}} ) @test_throws LMM.LocalMathValidationError LMM._validate_evaluator_result_type( - (publication,), NamedTuple{(:value,),Tuple{LMM.UniqueValue{Int32}}} + (publication,), NamedTuple{(:value,), Tuple{LMM.UniqueValue{Int32}}} ) @test_throws LMM.LocalMathValidationError LMM._validate_evaluator_result_type( (publication,), - NamedTuple{(:value,),Tuple{LMM.ConditionalUniqueValue{Float32}}}, + NamedTuple{(:value,), Tuple{LMM.ConditionalUniqueValue{Float32}}}, ) field_control = LMM.Control( @@ -185,11 +191,11 @@ struct SMForeignBounds <: LMM._ParameterBounds end @test reduce_publication.law === reduce @test LMM._validate_evaluator_result_type( (reduce_publication,), - NamedTuple{(:value,),Tuple{LMM.Contribution{Float32}}}, + NamedTuple{(:value,), Tuple{LMM.Contribution{Float32}}}, ) === nothing @test_throws LMM.LocalMathValidationError LMM._validate_evaluator_result_type( (reduce_publication,), - NamedTuple{(:value,),Tuple{LMM.UniqueValue{Float32}}}, + NamedTuple{(:value,), Tuple{LMM.UniqueValue{Float32}}}, ) @test_throws LMM.LocalMathValidationError LMM.Reduce( Float32, SMAdd(); seed = LMM.IdentitySeed(Int32(0)), @@ -211,11 +217,13 @@ struct SMForeignBounds <: LMM._ParameterBounds end resolve_publication = LMM.Publication((component,), resolve) @test LMM._validate_evaluator_result_type( (resolve_publication,), - NamedTuple{(:value,),Tuple{ - LMM.ResolutionValue{ - Int32,LMM._CanonicalOrdinal,Float32 - } - }}, + NamedTuple{ + (:value,), Tuple{ + LMM.ResolutionValue{ + Int32, LMM._CanonicalOrdinal, Float32, + }, + }, + }, ) === nothing @test_throws LMM.LocalMathValidationError LMM.Resolve( Int32, Float32; lower = Int32(2), upper = Int32(1), @@ -233,9 +241,11 @@ struct SMForeignBounds <: LMM._ParameterBounds end ) explicit_tie_publication = LMM.Publication((component,), explicit_tie) @test LMM._validate_evaluator_result_type( - (explicit_tie_publication,), NamedTuple{(:value,),Tuple{ - LMM.ResolutionValue{Int32,UInt32,Float32} - }}, + (explicit_tie_publication,), NamedTuple{ + (:value,), Tuple{ + LMM.ResolutionValue{Int32, UInt32, Float32}, + }, + }, ) === nothing wrong_space = LMM.Space(SMNode, 4) @@ -244,8 +254,10 @@ struct SMForeignBounds <: LMM._ParameterBounds end values_field, wrong_relation ) @test LMM.Access(values_field, identity).mode isa LMM._RequiredAccess - @test LMM.Access(values_field, identity; - required = false).mode isa LMM._SampleAccess + @test LMM.Access( + values_field, identity; + required = false + ).mode isa LMM._SampleAccess @test_throws LMM.LocalMathValidationError LMM.FieldPublication( values_field, wrong_relation, LMM.PublicationValue(:value) ) @@ -288,7 +300,7 @@ struct SMForeignBounds <: LMM._ParameterBounds end SMHostileParameterEvaluator{Ptr{Cvoid}}(), SMHostileParameterEvaluator{Base.RefValue{Int32}}(), SMHostileParameterEvaluator{ - NamedTuple{(:label,),Tuple{Int32}} + NamedTuple{(:label,), Tuple{Symbol}}, }(), SMHostileParameterEvaluator{typeof(values_field)}(), ) @@ -305,7 +317,7 @@ struct SMForeignBounds <: LMM._ParameterBounds end @test captured.actual.path == (:evaluator, :values) @test_throws MethodError LMM.PublicationValue{1}() @test_throws MethodError LMM.Parameter{ - Int32,SMForeignBounds + Int32, SMForeignBounds, }(:foreign, SMForeignBounds()) @test_throws LMM.LocalMathValidationError LMM.Parameter( LMM._STAGE_MODEL_SEAL, Int32, :foreign, SMForeignBounds() @@ -315,9 +327,13 @@ struct SMForeignBounds <: LMM._ParameterBounds end @test length(empty_nodes) == 0 empty_field = LMM.Field(empty_nodes, Float32) empty_identity = LMM.IdentityRelation(empty_nodes) - empty_publication = LMM.Publication((LMM.FieldPublication( - empty_field, empty_identity, LMM.PublicationValue(:empty_value) - ),), LMM.Unique(Float32)) + empty_publication = LMM.Publication( + ( + LMM.FieldPublication( + empty_field, empty_identity, LMM.PublicationValue(:empty_value) + ), + ), LMM.Unique(Float32) + ) empty_stage = LMM.Stage( empty_nodes, NamedTuple(), (empty_publication,), LMM.Evaluator(SMIdentityEvaluator()), LMM.Control(), From 121a38589b039c733109b157d87f9c6653537827 Mon Sep 17 00:00:00 2001 From: PraneethMerugu Date: Tue, 8 Sep 2026 17:09:39 -0400 Subject: [PATCH 2/4] Keep recursive storage admission in generated method bodies --- src/spatial_model.jl | 17 ++++++++++------- test/test_product_values.jl | 18 ++++++++++++++++-- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/spatial_model.jl b/src/spatial_model.jl index 9522388..e9dd0ee 100644 --- a/src/spatial_model.jl +++ b/src/spatial_model.jl @@ -12,9 +12,11 @@ _new_semantic_identity() = UUIDs.uuid4() _storage_value_type(::Type{T}) where {T <: Union{Number, Bool, Enum}} = isconcretetype(T) && isbitstype(T) @generated function _storage_value_type(::Type{T}) where {T <: Union{Tuple, NamedTuple}} - qualified = isconcretetype(T) && isbitstype(T) && - all(_storage_value_type, fieldtypes(T)) - return qualified ? :(true) : :(false) + isconcretetype(T) && isbitstype(T) || return :(false) + # Recursive dispatch belongs in the generated body, not its definition-time + # world: fields may use the generic record predicate defined below. + checks = [:(_storage_value_type($field_type)) for field_type in fieldtypes(T)] + return foldl((left, right) -> :($left && $right), checks; init = :(true)) end function _storage_value_type(::Type{T}) where {T <: StaticArrays.StaticArray} isconcretetype(T) && isbitstype(T) || return false @@ -39,10 +41,11 @@ end } ) && !(isdefined(Core, :LLVMPtr) && T <: Core.LLVMPtr) && - isstructtype(T) && - all(_storage_type_parameter, T.parameters) && - all(_storage_value_type, fieldtypes(T)) - return qualified ? :(true) : :(false) + isstructtype(T) + qualified || return :(false) + checks = [:(_storage_type_parameter($(QuoteNode(parameter)))) for parameter in T.parameters] + append!(checks, [:(_storage_value_type($field_type)) for field_type in fieldtypes(T)]) + return foldl((left, right) -> :($left && $right), checks; init = :(true)) end function _checked_semantic_int(value::Integer, purpose::Symbol; positive = false) diff --git a/test/test_product_values.jl b/test/test_product_values.jl index e6f5065..a7d694a 100644 --- a/test/test_product_values.jl +++ b/test/test_product_values.jl @@ -9,6 +9,14 @@ struct ProductValueUpdate{P} increment::P end +struct TupleProductValueUpdate{T} + operations::T +end + +@inline function (operation::TupleProductValueUpdate)(item::Int32, reads, parameters) + return only(operation.operations)(item, reads, parameters) +end + @inline function (operation::ProductValueUpdate)(item::Int32, reads, parameters) before = something(reads[1][1].value) increment = operation.increment @@ -20,7 +28,7 @@ end return (value = LocalMath.UniqueValue(after),) end -function test_product_value_publication(backend) +function test_product_value_publication(backend; wrap = identity) initial = (active = false, count = Int32(2), polarity = SVector(1.0f0, 2.0f0)) increment = (count = Int32(3), polarity = SVector(0.5f0, 1.0f0)) space = LocalMath.Space(3) @@ -30,7 +38,7 @@ function test_product_value_publication(backend) stage = LocalMath.Stage( space, (value = LocalMath.Access(input, relation; required = true),), (LocalMath.Publication((LocalMath.FieldPublication(output, relation, LocalMath.PublicationValue(:value)),), LocalMath.Unique(typeof(initial))),), - LocalMath.Evaluator(ProductValueUpdate(increment)), LocalMath.Control(), + LocalMath.Evaluator(wrap(ProductValueUpdate(increment))), LocalMath.Control(), LocalMath.SourceOrigin(:product_value_publication, 1), ) prepared = LocalMath.prepare( @@ -46,9 +54,15 @@ end @testset "named products share ordinary field publication" begin test_product_value_publication(KernelAbstractions.CPU()) + test_product_value_publication( + KernelAbstractions.CPU(); wrap = operation -> TupleProductValueUpdate((operation,)) + ) for value in ((name = :metadata,), (values = Float32[1],), (pointer = Ptr{Float32}(0),)) @test_throws LocalMath.LocalMathValidationError LocalMath.Field(LocalMath.Space(1), typeof(value)) @test_throws LocalMath.LocalMathValidationError LocalMath.Evaluator(ProductValueUpdate(value)) + @test_throws LocalMath.LocalMathValidationError LocalMath.Evaluator( + TupleProductValueUpdate((ProductValueUpdate(value),)) + ) end end From df9a650ac8058f71c41ae80f36b26862e880147c Mon Sep 17 00:00:00 2001 From: PraneethMerugu Date: Tue, 8 Sep 2026 17:34:03 -0400 Subject: [PATCH 3/4] Honor ordered-fold gates before evaluation and publication --- CONTRIBUTING.md | 8 +- docs/src/api/localmath.md | 6 + src/execution/ordered_fold_stage.jl | 501 ++++++++++++------ .../ordered_fold_control_contracts.jl | 101 ++++ test/metal/ordered_fold_control.jl | 5 + test/metal/runtests.jl | 1 + test/runtests.jl | 1 + test/test_ordered_fold_control.jl | 2 + 8 files changed, 462 insertions(+), 163 deletions(-) create mode 100644 test/fixtures/ordered_fold_control_contracts.jl create mode 100644 test/metal/ordered_fold_control.jl create mode 100644 test/test_ordered_fold_control.jl diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0360e16..1dba1b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,9 +82,15 @@ the changed behavior. For example, a focused root check can load the shared setup explicitly: ```sh -julia --project=. --startup-file=no -e 'include("test/setup.jl"); include("test/test_public_api.jl")' +julia --project=. --startup-file=no -e 'using Test; import LocalMath; include("test/support.jl"); include("test/test_public_api.jl")' ``` +Ordered-fold participation and publication are owned by +`src/execution/ordered_fold_stage.jl`. The shared behavioral fixture +`test/fixtures/ordered_fold_control_contracts.jl` exercises closed/open gates, +retained destinations, and unchanged duplicate-order rejection through the +ordinary CPU and Metal inventories. + Focused commands shorten the edit loop; they are not a second test inventory or release gate. Before handoff, run the complete suite of every changed package. Add the integration suite when a package boundary, extension, SciML diff --git a/docs/src/api/localmath.md b/docs/src/api/localmath.md index 97bcbe0..b669fa6 100644 --- a/docs/src/api/localmath.md +++ b/docs/src/api/localmath.md @@ -86,6 +86,12 @@ scratch, and a lifetime wholly contained by one pointwise segment is forwarded without writing that scratch. Ordinary user Fields remain explicitly bound and observable. +An `OrderedFold` stage with a closed `Control` gate does not evaluate or order +events, run its recurrence, or publish its initializer over the retained +destination. This holds for parameter gates and gates produced by a preceding +total Field publication. Opening the gate restores ordinary ordered-fold +validation, including rejection of duplicate ordering identities. + ## Bounded scalar operators `LocalMath.fold` names the mathematical action and takes an already bounded diff --git a/src/execution/ordered_fold_stage.jl b/src/execution/ordered_fold_stage.jl index c926549..de593f7 100644 --- a/src/execution/ordered_fold_stage.jl +++ b/src/execution/ordered_fold_stage.jl @@ -5,17 +5,17 @@ const _ORDERED_FOLD_BLOCK = 256 -struct _OrderedFoldStageWorkspace{V,O,S,X,R,T} +struct _OrderedFoldStageWorkspace{V, O, S, X, R, T} values::V; order::O; status::S; validation::X; state::R; tree::T end -struct _OrderedFoldRecurrenceStage{F,A,P,G} +struct _OrderedFoldRecurrenceStage{F, A, P, G} fields::F accesses::A prefix::P gate::G source_count::Int32 end -struct _OrderedFoldStagePreparation{B,S,W,V} +struct _OrderedFoldStagePreparation{B, S, W, V} backend::B; stage::S; workspace::W; validation::V state_extent::Int32 end @@ -30,73 +30,101 @@ function _ordered_fold_stage_workspace_spec(stage; path::Tuple, name_prefix::Sym n = Int(stage.source_count) order_capacity = nextpow(2, max(n, 1)) T = _publication_value_type(law) - names = (values = Symbol(name_prefix, :_values), order = Symbol(name_prefix, :_order), + names = ( + values = Symbol(name_prefix, :_values), order = Symbol(name_prefix, :_order), status = Symbol(name_prefix, :_status), - validation = Symbol(name_prefix, :_validation)) + validation = Symbol(name_prefix, :_validation), + ) base_leaves = ( _workspace_leaf(names.values, (path..., :values), T, (n,); role = :ordered_fold_value), - _workspace_leaf(names.order, (path..., :order), Int32, - (order_capacity,); role = :ordered_fold_order), + _workspace_leaf( + names.order, (path..., :order), Int32, + (order_capacity,); role = :ordered_fold_order + ), _workspace_leaf(names.status, (path..., :status), Int32, (1,); role = :ordered_fold_diagnostic), - _workspace_leaf(names.validation, (path..., :validation), UInt32, - (_VALIDATION_STATUS_FIELDS, 1); role = :validation_status), + _workspace_leaf( + names.validation, (path..., :validation), UInt32, + (_VALIDATION_STATUS_FIELDS, 1); role = :validation_status + ), ) state_leaves = map(eachindex(state_names)) do index component = getfield(state.components, index) storage = _prepared_stage_field(stage.fields, component.target) name = Symbol(name_prefix, :_state_, state_names[index]) - _workspace_leaf(name, (path..., :state, state_names[index]), - eltype(storage), size(storage); role = :ordered_fold_state) + _workspace_leaf( + name, (path..., :state, state_names[index]), + eltype(storage), size(storage); role = :ordered_fold_state + ) end leaves = (base_leaves..., state_leaves...) - state_template = NamedTuple{state_names}(Tuple(map(eachindex(state_names)) do index - _WorkspaceLeafSlot(Symbol( - name_prefix, :_state_, state_names[index])) - end)) - template = (; values = _WorkspaceLeafSlot(names.values), + state_template = NamedTuple{state_names}( + Tuple( + map(eachindex(state_names)) do index + _WorkspaceLeafSlot( + Symbol( + name_prefix, :_state_, state_names[index] + ) + ) + end + ) + ) + template = (; + values = _WorkspaceLeafSlot(names.values), order = _WorkspaceLeafSlot(names.order), status = _WorkspaceLeafSlot(names.status), validation = _WorkspaceLeafSlot(names.validation), - state = state_template) + state = state_template, + ) return (; leaves, template) end function _ordered_fold_stage_workspace_from_tree(tree, spec) - _OrderedFoldStageWorkspace(tree.values, tree.order, tree.status, - tree.validation, tree.state, tree) + return _OrderedFoldStageWorkspace( + tree.values, tree.order, tree.status, + tree.validation, tree.state, tree + ) end -@inline function _ordered_fold_stage_fail!(run, code::Int32, +@inline function _ordered_fold_stage_fail!( + run, code::Int32, component::Int32 = Int32(0), source_item::Int32 = Int32(0), - position::Int32 = Int32(0), witness::Int32 = Int32(0)) + position::Int32 = Int32(0), witness::Int32 = Int32(0) + ) @inbounds run.workspace.status[1] = code - _store_validation_status!(run.workspace.validation, + _store_validation_status!( + run.workspace.validation, run.lease_index, code, component, source_item, position, - reinterpret(UInt32, witness)) + reinterpret(UInt32, witness) + ) return nothing end @inline _ordered_fold_stage_success(run) = @inbounds(run.workspace.status[1]) == 0 @inline _ordered_fold_stage_prefix_ok(run) = _candidate_prefix_succeeded(run.predecessors, run.lease_index) @generated function _stage_reads( - stage::_OrderedFoldRecurrenceStage{F,A}, item::Int32, - ) where {F,A<:Tuple} + stage::_OrderedFoldRecurrenceStage{F, A}, item::Int32, + ) where {F, A <: Tuple} reads = map(1:fieldcount(A)) do index :(_stage_read(stage, getfield(stage.accesses, $index), item)) end return Expr(:tuple, reads...) end @generated function _stage_reads( - stage::_OrderedFoldRecurrenceStage{F,A}, item::Int32, validation, - ) where {F,A<:Tuple} + stage::_OrderedFoldRecurrenceStage{F, A}, item::Int32, validation, + ) where {F, A <: Tuple} reads = map(1:fieldcount(A)) do index - :(_stage_read(stage, getfield(stage.accesses, $index), item, - validation)) + :( + _stage_read( + stage, getfield(stage.accesses, $index), item, + validation + ) + ) end return Expr(:tuple, reads...) end @kernel function _ordered_fold_stage_reset_kernel!( - order, status, validation, lease_index, extent::Int32) + order, status, validation, lease_index, extent::Int32 + ) index = @index(Global, Linear) index <= extent && (@inbounds order[index] = Int32(0)) if index == 1 @@ -111,20 +139,23 @@ end names = C.parameters[1] body = Any[] for i in eachindex(names) - push!(body, quote - component = getfield(state.components, $i) - target = getfield(scratch, $(QuoteNode(names[i]))) - source = _prepared_stage_field(fields, component.source) - item <= length(target) && - (@inbounds target[item] = source[item]) - end) + push!( + body, quote + component = getfield(state.components, $i) + target = getfield(scratch, $(QuoteNode(names[i]))) + source = _prepared_stage_field(fields, component.source) + item <= length(target) && + (@inbounds target[item] = source[item]) + end + ) end - Expr(:block, body..., :(nothing)) + return Expr(:block, body..., :(nothing)) end @kernel function _ordered_fold_stage_validate_initialize_kernel!( order_law, state, fields, workspace, lease_index::Int32, - predecessors, order_extent::Int32, state_extent::Int32) + predecessors, order_extent::Int32, state_extent::Int32 + ) item = @index(Global, Linear) if _candidate_prefix_succeeded(predecessors, lease_index) if Int32(2) <= item <= order_extent @@ -132,40 +163,54 @@ end previous = @inbounds order[item - 1] current = @inbounds order[item] if previous != 0 && current != 0 && _ordered_fold_stage_equal( - order_law, workspace.values, previous, current) - _candidate_atomic_max!(workspace.status, 1, - Int32(_ORDERED_FOLD_DUPLICATE_ORDER)) + order_law, workspace.values, previous, current + ) + _candidate_atomic_max!( + workspace.status, 1, + Int32(_ORDERED_FOLD_DUPLICATE_ORDER) + ) end end # Initialization targets only private shadow state. It is deliberately # unconditional with respect to order validation; recurrence is a later # launch and observes the completed diagnostic status. item <= state_extent && _ordered_fold_stage_initialize_item!( - state, fields, workspace.state, Int32(item)) + state, fields, workspace.state, Int32(item) + ) end end @kernel function _ordered_fold_stage_evaluate_kernel!( qualified, workspace, lease_index::Int32, predecessors, - extent::Int32) + extent::Int32 + ) item = @index(Global, Linear) + stage = qualified.stage if item <= extent && - _candidate_prefix_succeeded(predecessors, lease_index) - stage = qualified.stage + _candidate_prefix_succeeded(predecessors, lease_index) && + _stage_gate_open(stage.control.gate, stage, qualified.parameters) valid_control, enabled = _stage_control_state( - stage, qualified.parameters, Int32(item)) + stage, qualified.parameters, Int32(item) + ) if !valid_control - _candidate_atomic_max!(workspace.status, 1, - Int32(_CANDIDATE_STATUS_INVALID_CONTROL)) + _candidate_atomic_max!( + workspace.status, 1, + Int32(_CANDIDATE_STATUS_INVALID_CONTROL) + ) elseif !_stage_accesses_valid( - stage.accesses, stage.fields, Int32(item)) - _candidate_atomic_max!(workspace.status, 1, - Int32(_CANDIDATE_STATUS_RELATION)) + stage.accesses, stage.fields, Int32(item) + ) + _candidate_atomic_max!( + workspace.status, 1, + Int32(_CANDIDATE_STATUS_RELATION) + ) elseif enabled validation = _OrderedFoldEvaluationValidation(workspace.status) - result = _call_stage_evaluator(qualified, Int32(item), + result = _call_stage_evaluator( + qualified, Int32(item), _stage_reads(stage, Int32(item), validation), - qualified.parameters) + qualified.parameters + ) value = getfield(result, 1) if value.participates @inbounds begin @@ -177,75 +222,104 @@ end end end -@inline function _ordered_fold_stage_validate_writes(writes::BoundedWrites{K}, extent, - component::Int32) where {K} +@inline function _ordered_fold_stage_validate_writes( + writes::BoundedWrites{K}, extent, + component::Int32 + ) where {K} 0 <= writes.count <= K || return ( - Int32(_ORDERED_FOLD_UPDATE_COUNT), Int32(writes.count)) + Int32(_ORDERED_FOLD_UPDATE_COUNT), Int32(writes.count), + ) for left in Int32(1):writes.count key = @inbounds writes.keys[left] 1 <= key <= extent || return ( - Int32(_ORDERED_FOLD_DESTINATION), Int32(key)) + Int32(_ORDERED_FOLD_DESTINATION), Int32(key), + ) for right in Int32(1):(left - 1) key == @inbounds(writes.keys[right]) && return ( - Int32(_ORDERED_FOLD_DUPLICATE_UPDATE), Int32(key)) + Int32(_ORDERED_FOLD_DUPLICATE_UPDATE), Int32(key), + ) end end return (Int32(0), Int32(0)) end -@generated function _ordered_fold_stage_validate_step!(state::_PreparedFoldState{C}, scratch, - step::FoldStep) where {C} +@generated function _ordered_fold_stage_validate_step!( + state::_PreparedFoldState{C}, scratch, + step::FoldStep + ) where {C} names = C.parameters[1] checks = Any[] for i in eachindex(names) - push!(checks, quote - component = getfield(state.components, $i) - code, witness = _ordered_fold_stage_validate_writes(getfield(step.updates, $(QuoteNode(names[i]))), - length(getfield(scratch, $(QuoteNode(names[i])))), Int32($i)) - code == 0 || return (code, Int32($i), witness) - end) + push!( + checks, quote + component = getfield(state.components, $i) + code, witness = _ordered_fold_stage_validate_writes( + getfield(step.updates, $(QuoteNode(names[i]))), + length(getfield(scratch, $(QuoteNode(names[i])))), Int32($i) + ) + code == 0 || return (code, Int32($i), witness) + end + ) end - Expr(:block, checks..., :((Int32(0), Int32(0), Int32(0)))) + return Expr(:block, checks..., :((Int32(0), Int32(0), Int32(0)))) end @inline function _ordered_fold_stage_apply_writes!(storage, writes::BoundedWrites) for j in Int32(1):writes.count @inbounds storage[writes.keys[j]] = writes.values[j] end + return end -@generated function _ordered_fold_stage_apply_step!(state::_PreparedFoldState{C}, scratch, - step::FoldStep) where {C} +@generated function _ordered_fold_stage_apply_step!( + state::_PreparedFoldState{C}, scratch, + step::FoldStep + ) where {C} names = C.parameters[1] - Expr(:block, [:(begin - component = getfield(state.components, $i) - _ordered_fold_stage_apply_writes!(getfield(scratch, $(QuoteNode(names[i]))), - getfield(step.updates, $(QuoteNode(names[i])))) - end) for i in eachindex(names)]..., :(nothing)) + return Expr( + :block, [ + :( + begin + component = getfield(state.components, $i) + _ordered_fold_stage_apply_writes!( + getfield(scratch, $(QuoteNode(names[i]))), + getfield(step.updates, $(QuoteNode(names[i]))) + ) + end + ) for i in eachindex(names) + ]..., :(nothing) + ) end @inline function _ordered_fold_stage_less(order, values, left, right) order isa _SourceOrder && return left < right - comparison = _canonical_order_compare(_ordering_extract(order.key, @inbounds(values[left])), + comparison = _canonical_order_compare( + _ordering_extract(order.key, @inbounds(values[left])), _ordering_extract(order.identity, @inbounds(values[left])), _ordering_extract(order.key, @inbounds(values[right])), - _ordering_extract(order.identity, @inbounds(values[right]))) + _ordering_extract(order.identity, @inbounds(values[right])) + ) return comparison < 0 end @inline function _ordered_fold_stage_equal(order, values, left, right) order isa _SourceOrder && return false - _canonical_order_equal(_ordering_extract(order.key, @inbounds(values[left])), + return _canonical_order_equal( + _ordering_extract(order.key, @inbounds(values[left])), _ordering_extract(order.identity, @inbounds(values[left])), _ordering_extract(order.key, @inbounds(values[right])), - _ordering_extract(order.identity, @inbounds(values[right]))) + _ordering_extract(order.identity, @inbounds(values[right])) + ) end -@inline function _ordered_fold_stage_index_less(order, values, - left::Int32, right::Int32) +@inline function _ordered_fold_stage_index_less( + order, values, + left::Int32, right::Int32 + ) left == 0 && return false right == 0 && return true return _ordered_fold_stage_less(order, values, left, right) end @kernel function _ordered_fold_stage_bitonic_kernel!( - order_law, values, order, distance::Int32, width::Int32, extent::Int32) + order_law, values, order, distance::Int32, width::Int32, extent::Int32 + ) item = @index(Global, Linear) if item <= extent partner = Int32(xor(item - 1, distance)) + Int32(1) @@ -253,9 +327,11 @@ end left, right = @inbounds(order[item]), @inbounds(order[partner]) ascending = (Int32(item - 1) & width) == 0 right_less = _ordered_fold_stage_index_less( - order_law, values, right, left) + order_law, values, right, left + ) left_less = _ordered_fold_stage_index_less( - order_law, values, left, right) + order_law, values, left, right + ) swap = ascending ? right_less : left_less if swap @inbounds order[item], order[partner] = right, left @@ -270,11 +346,15 @@ function _ordered_fold_stage_launch_order!(backend, order_law, workspace) while width <= extent distance = width >>> 1 while distance >= 1 - _ordered_fold_stage_bitonic_kernel!(backend, - min(extent, _ORDERED_FOLD_BLOCK), extent)(order_law, + _ordered_fold_stage_bitonic_kernel!( + backend, + min(extent, _ORDERED_FOLD_BLOCK), extent + )( + order_law, workspace.values, workspace.order, Int32(distance), Int32(width), Int32(extent); - ndrange = extent) + ndrange = extent + ) distance >>>= 1 end width <<= 1 @@ -285,11 +365,14 @@ end @inline function _ordered_fold_stage_execute!(run) workspace = run.workspace _ordered_fold_stage_prefix_ok(run) || return - gate = _stage_gate_open(run.stage.gate, run.stage, - run.boundary_parameters) + gate = _stage_gate_open( + run.stage.gate, run.stage, + run.boundary_parameters + ) gate || return prefix = _stage_prefix_value( - run.stage.prefix, run.stage, run.boundary_parameters) + run.stage.prefix, run.stage, run.boundary_parameters + ) prefix isa Integer && !(prefix isa Bool) && 0 <= prefix <= run.stage.source_count || return _ordered_fold_stage_fail!(run, Int32(_CANDIDATE_STATUS_INVALID_CONTROL)) @@ -298,19 +381,27 @@ end for position in Int32(1):run.stage.source_count item = @inbounds workspace.order[position] item == 0 && break - step = run.transition(accumulator, + step = run.transition( + accumulator, @inbounds(workspace.values[item]), item, - _stage_reads(run.stage, item, - _OrderedFoldEvaluationValidation(workspace.status))) + _stage_reads( + run.stage, item, + _OrderedFoldEvaluationValidation(workspace.status) + ) + ) _ordered_fold_stage_success(run) || return code, component_index, witness = _ordered_fold_stage_validate_step!( - run.state, run.workspace.state, step) - code == 0 || return _ordered_fold_stage_fail!(run, code, - component_index, item, position, witness) + run.state, run.workspace.state, step + ) + code == 0 || return _ordered_fold_stage_fail!( + run, code, + component_index, item, position, witness + ) _ordered_fold_stage_apply_step!(run.state, run.workspace.state, step) step.halt && return end + return end @generated function _ordered_fold_stage_commit_item!( @@ -326,7 +417,7 @@ end (@inbounds target[item] = source[item]) end end - Expr(:block, copies..., :(nothing)) + return Expr(:block, copies..., :(nothing)) end @kernel function _ordered_fold_stage_kernel!(run) @@ -340,72 +431,133 @@ end code = @inbounds run.workspace.status[1] if index == 1 validation = run.workspace.validation - existing = @inbounds validation[_VALIDATION_FAILURE_CLASS, - run.lease_index] + existing = @inbounds validation[ + _VALIDATION_FAILURE_CLASS, + run.lease_index, + ] if existing != UInt32(0) - _store_program_validation_status!(run.program_validation, + _store_program_validation_status!( + run.program_validation, run.lease_index, existing, - @inbounds(validation[_VALIDATION_CONTEXT_INDEX, - run.lease_index]), - @inbounds(validation[_VALIDATION_PRIMARY_RECORD, - run.lease_index]), - @inbounds(validation[_VALIDATION_SECONDARY_RECORD, - run.lease_index]), - @inbounds(validation[_VALIDATION_WITNESS_BITS, - run.lease_index])) + @inbounds( + validation[ + _VALIDATION_CONTEXT_INDEX, + run.lease_index, + ] + ), + @inbounds( + validation[ + _VALIDATION_PRIMARY_RECORD, + run.lease_index, + ] + ), + @inbounds( + validation[ + _VALIDATION_SECONDARY_RECORD, + run.lease_index, + ] + ), + @inbounds( + validation[ + _VALIDATION_WITNESS_BITS, + run.lease_index, + ] + ) + ) elseif code == Int32(_ORDERED_FOLD_DUPLICATE_ORDER) order = run.workspace.order for position in Int32(2):run.source_count previous = @inbounds order[position - 1] item = @inbounds order[position] item == 0 && break - if previous != 0 && _ordered_fold_stage_equal(run.order, - run.workspace.values, previous, item) - _store_validation_status!(validation, run.lease_index, + if previous != 0 && _ordered_fold_stage_equal( + run.order, + run.workspace.values, previous, item + ) + _store_validation_status!( + validation, run.lease_index, code, Int32(0), previous, position - Int32(1), - reinterpret(UInt32, item)) - _store_program_validation_status!(run.program_validation, + reinterpret(UInt32, item) + ) + _store_program_validation_status!( + run.program_validation, run.lease_index, code, Int32(0), previous, - position - Int32(1), reinterpret(UInt32, item)) + position - Int32(1), reinterpret(UInt32, item) + ) break end end elseif existing == UInt32(0) - _store_validation_status!(validation, run.lease_index, - code, Int32(0), code, Int32(0), reinterpret(UInt32, code)) - _store_program_validation_status!(run.program_validation, + _store_validation_status!( + validation, run.lease_index, + code, Int32(0), code, Int32(0), reinterpret(UInt32, code) + ) + _store_program_validation_status!( + run.program_validation, run.lease_index, code, Int32(0), code, Int32(0), - reinterpret(UInt32, code)) + reinterpret(UInt32, code) + ) end end - code == 0 && _ordered_fold_stage_commit_item!(run.state, - run.stage.fields, run.workspace.state, Int32(index)) + # Initialization copies only private scratch; a closed gate must not + # publish that initializer over the destination's retained value. + if code == 0 && _stage_gate_open( + run.stage.gate, run.stage, + run.boundary_parameters + ) + _ordered_fold_stage_commit_item!( + run.state, + run.stage.fields, run.workspace.state, Int32(index) + ) + end end end -function _prepare_ordered_fold_stage(admission::_StageAdmission, - workspace::_OrderedFoldStageWorkspace) +function _prepare_ordered_fold_stage( + admission::_StageAdmission, + workspace::_OrderedFoldStageWorkspace + ) stage = admission.stage - only(stage.publications).law isa _PreparedOrderedFoldLaw || throw(LocalMathValidationError( - "OrderedFold executor requires one prepared terminal OrderedFold law"; - stage = :prepare, contract = :ordered_fold_stage_law)) + only(stage.publications).law isa _PreparedOrderedFoldLaw || throw( + LocalMathValidationError( + "OrderedFold executor requires one prepared terminal OrderedFold law"; + stage = :prepare, contract = :ordered_fold_stage_law + ) + ) state = only(only(stage.publications).components).state - state_extent = maximum((length(_prepared_stage_field(stage.fields, - component.target)) for component in values(state.components)); - init = 0) - state_extent <= typemax(Int32) || throw(LocalMathValidationError( - "OrderedFold state extent exceeds its device index type"; - stage = :prepare, contract = :ordered_fold_state_extent, - expected = 0:typemax(Int32), actual = state_extent)) - _OrderedFoldStagePreparation(admission.backend, stage, workspace, - workspace.validation, Int32(state_extent)) + state_extent = maximum( + ( + length( + _prepared_stage_field( + stage.fields, + component.target + ) + ) for component in values(state.components) + ); + init = 0 + ) + state_extent <= typemax(Int32) || throw( + LocalMathValidationError( + "OrderedFold state extent exceeds its device index type"; + stage = :prepare, contract = :ordered_fold_state_extent, + expected = 0:typemax(Int32), actual = state_extent + ) + ) + return _OrderedFoldStagePreparation( + admission.backend, stage, workspace, + workspace.validation, Int32(state_extent) + ) end -function _execute_ordered_fold_stage!(prepared::_OrderedFoldStagePreparation, +function _execute_ordered_fold_stage!( + prepared::_OrderedFoldStagePreparation, parameters::Tuple, lease_index::Int32, predecessors::Tuple, - relation_guard, program_validation) - qualified = _QualifiedEvaluation(_stage_evaluation(prepared.stage), - _stage_runtime_parameters(parameters, prepared.stage)) + relation_guard, program_validation + ) + qualified = _QualifiedEvaluation( + _stage_evaluation(prepared.stage), + _stage_runtime_parameters(parameters, prepared.stage) + ) prefix = (relation_guard, predecessors...) publication = only(prepared.stage.publications) state = only(publication.components).state @@ -413,44 +565,69 @@ function _execute_ordered_fold_stage!(prepared::_OrderedFoldStagePreparation, recurrence_stage = _OrderedFoldRecurrenceStage( prepared.stage.fields, prepared.stage.accesses, prepared.stage.control.prefix, prepared.stage.control.gate, - prepared.stage.source_count) - boundary_parameters = (; prefix = qualified.parameters.prefix, - gate = qualified.parameters.gate) - recurrence = (; stage = recurrence_stage, + prepared.stage.source_count + ) + boundary_parameters = (; + prefix = qualified.parameters.prefix, + gate = qualified.parameters.gate, + ) + recurrence = (; + stage = recurrence_stage, boundary_parameters, transition = law.transition, state, workspace = prepared.workspace, - lease_index, predecessors = prefix) - finalization = (; order = law.order, + lease_index, predecessors = prefix, + ) + finalization = (; + order = law.order, source_count = prepared.stage.source_count, - stage = recurrence_stage, state, + stage = recurrence_stage, boundary_parameters, state, workspace = prepared.workspace, lease_index, - predecessors = prefix, program_validation) + predecessors = prefix, program_validation, + ) order_extent = length(prepared.workspace.order) - _ordered_fold_stage_reset_kernel!(prepared.backend, - min(order_extent, _ORDERED_FOLD_BLOCK), order_extent)( + _ordered_fold_stage_reset_kernel!( + prepared.backend, + min(order_extent, _ORDERED_FOLD_BLOCK), order_extent + )( prepared.workspace.order, prepared.workspace.status, prepared.workspace.validation, lease_index, Int32(order_extent); - ndrange = order_extent) - _launch_stage_relation_receipt!(prepared.backend, relation_guard, - prepared.validation, program_validation, lease_index) + ndrange = order_extent + ) + _launch_stage_relation_receipt!( + prepared.backend, relation_guard, + prepared.validation, program_validation, lease_index + ) source_extent = max(Int(prepared.stage.source_count), 1) - _ordered_fold_stage_evaluate_kernel!(prepared.backend, - min(source_extent, _ORDERED_FOLD_BLOCK), source_extent)(qualified, + _ordered_fold_stage_evaluate_kernel!( + prepared.backend, + min(source_extent, _ORDERED_FOLD_BLOCK), source_extent + )( + qualified, prepared.workspace, lease_index, prefix, - prepared.stage.source_count; ndrange = source_extent) - _ordered_fold_stage_launch_order!(prepared.backend, law.order, - prepared.workspace) + prepared.stage.source_count; ndrange = source_extent + ) + _ordered_fold_stage_launch_order!( + prepared.backend, law.order, + prepared.workspace + ) state_extent = prepared.state_extent initialize_extent = max(source_extent, Int(state_extent), 1) - _ordered_fold_stage_validate_initialize_kernel!(prepared.backend, - min(initialize_extent, _ORDERED_FOLD_BLOCK), initialize_extent)( + _ordered_fold_stage_validate_initialize_kernel!( + prepared.backend, + min(initialize_extent, _ORDERED_FOLD_BLOCK), initialize_extent + )( law.order, state, prepared.stage.fields, prepared.workspace, lease_index, prefix, prepared.stage.source_count, state_extent; - ndrange = initialize_extent) + ndrange = initialize_extent + ) _ordered_fold_stage_kernel!(prepared.backend)(recurrence; ndrange = 1) state_launch = max(Int(state_extent), 1) - _ordered_fold_stage_finalize_kernel!(prepared.backend, - min(state_launch, _ORDERED_FOLD_BLOCK), state_launch)(finalization; - ndrange = state_launch) + _ordered_fold_stage_finalize_kernel!( + prepared.backend, + min(state_launch, _ORDERED_FOLD_BLOCK), state_launch + )( + finalization; + ndrange = state_launch + ) return prepared end diff --git a/test/fixtures/ordered_fold_control_contracts.jl b/test/fixtures/ordered_fold_control_contracts.jl new file mode 100644 index 0000000..c979cf4 --- /dev/null +++ b/test/fixtures/ordered_fold_control_contracts.jl @@ -0,0 +1,101 @@ +using Test +import LocalMath +import KernelAbstractions + +struct OrderedFoldControlDomain end +struct OrderedFoldControlEvent end +@inline (::OrderedFoldControlEvent)(item::Int32, reads, parameters) = + (event = LocalMath.FoldValue(item),) +struct OrderedFoldControlWrite end +@inline (::OrderedFoldControlWrite)(state, value, item, reads) = LocalMath.FoldStep( + (result = LocalMath.BoundedWrites((Int32(1),), (value,), Int32(1)),), +) +struct OrderedFoldDuplicateIdentity end +@inline (::OrderedFoldDuplicateIdentity)(value) = Int32(0) +struct OrderedFoldControlGate end +@inline (::OrderedFoldControlGate)(item::Int32, reads, parameters) = + (gate = LocalMath.UniqueValue(something(reads[1][1].value)),) + +function ordered_fold_control_contracts(array_type) + return @testset "closed ordered-fold gates preserve publication" begin + for gate_kind in (:field, :parameter), duplicate in (false, true) + @testset "$gate_kind duplicate=$duplicate" begin + source = LocalMath.Space(OrderedFoldControlDomain, 5) + state_space = LocalMath.Space(OrderedFoldControlDomain, 2) + initial, result = LocalMath.Field(state_space, Int32), LocalMath.Field(state_space, Int32) + gate_space = LocalMath.Space(1) + external_gate = LocalMath.Field(gate_space, Bool) + gate = gate_kind === :field ? LocalMath.Field(gate_space, Bool) : + LocalMath.Parameter(:enabled, Bool) + parameters = gate_kind === :parameter ? (gate,) : () + state = LocalMath.InitializedState(; + result = LocalMath.FoldComponent(result; from = initial), + ) + key = duplicate ? OrderedFoldDuplicateIdentity() : identity + fold = LocalMath.OrderedFold( + Int32, state, OrderedFoldControlWrite(); + order = LocalMath.canonical_by(key, key), + ) + stage = LocalMath.Stage( + source, NamedTuple(), + (LocalMath.Publication((LocalMath.FoldPublication(LocalMath.PublicationValue(:event)),), fold),), + LocalMath.Evaluator(OrderedFoldControlEvent(), parameters), + LocalMath.Control(; gate), + LocalMath.SourceOrigin(@__FILE__, @__LINE__; label = :ordered_fold_control), + ) + destination = array_type(Int32[66, 77]) + initial_values = array_type(Int32[7, 8]) + enabled_values = array_type(Bool[false]) + bindings = (initial => initial_values, result => destination) + work = LocalMath.LocalLaw(stage) + if gate_kind === :field + relation = LocalMath.IdentityRelation(gate_space) + copy_gate = LocalMath.Stage( + gate_space, (gate = LocalMath.Access(external_gate, relation; required = true),), + ( + LocalMath.Publication( + (LocalMath.FieldPublication(gate, relation, LocalMath.PublicationValue(:gate)),), + LocalMath.Unique(Bool), + ), + ), + LocalMath.Evaluator(OrderedFoldControlGate()), LocalMath.Control(), + LocalMath.SourceOrigin(@__FILE__, @__LINE__; label = :ordered_fold_gate_publication), + ) + work = LocalMath.sequence(LocalMath.LocalLaw(copy_gate), work) + bindings = (bindings..., external_gate => enabled_values, gate => LocalMath.Allocate(false)) + end + prepared = LocalMath.prepare( + work, bindings...; + backend = KernelAbstractions.get_backend(destination), + ) + function run(enabled) + gate_kind === :field && copyto!(enabled_values, array_type(Bool[enabled])) + arguments = gate_kind === :parameter ? (; parameters = (; enabled)) : NamedTuple() + return try + wait(LocalMath.execute!(prepared; arguments...)) + nothing + catch error + error + end + end + @test run(false) === nothing + @test Array(destination) == Int32[66, 77] + failure = run(true) + if duplicate + @test failure isa LocalMath.LocalMathValidationError + @test failure.actual.failure_class === :duplicate_order_identity + @test Array(destination) == Int32[66, 77] + else + @test failure === nothing + @test Array(destination) == Int32[5, 8] + copyto!(destination, array_type(Int32[91, 92])) + copyto!(initial_values, array_type(Int32[13, 14])) + @test run(false) === nothing + @test Array(destination) == Int32[91, 92] + @test run(true) === nothing + @test Array(destination) == Int32[5, 14] + end + end + end + end +end diff --git a/test/metal/ordered_fold_control.jl b/test/metal/ordered_fold_control.jl new file mode 100644 index 0000000..d212171 --- /dev/null +++ b/test/metal/ordered_fold_control.jl @@ -0,0 +1,5 @@ +using Metal +include(joinpath(@__DIR__, "..", "fixtures", "ordered_fold_control_contracts.jl")) +Metal.functional() || error("ordered-fold control tests require functional Metal") +Metal.allowscalar(false) +ordered_fold_control_contracts(Metal.MtlArray) diff --git a/test/metal/runtests.jl b/test/metal/runtests.jl index 0e12195..cb1129a 100644 --- a/test/metal/runtests.jl +++ b/test/metal/runtests.jl @@ -14,6 +14,7 @@ const LOCALMATH_METAL_WITNESSES = ( "localmath_correctness.jl", "destination_grouping.jl", "product_values.jl", + "ordered_fold_control.jl", ) @testset "LocalMath Metal runner inventory" begin diff --git a/test/runtests.jl b/test/runtests.jl index f4d48f5..124712f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -22,6 +22,7 @@ const LOCALMATH_INCLUDED_TESTS = ( "test_collect_stage_execution.jl", "test_ordered_fold_stage_model.jl", "test_ordered_fold_stage_execution.jl", + "test_ordered_fold_control.jl", "test_stage_failure_barrier.jl", "test_stage_collection_binding.jl", "test_collection_stage_access.jl", diff --git a/test/test_ordered_fold_control.jl b/test/test_ordered_fold_control.jl new file mode 100644 index 0000000..28963b3 --- /dev/null +++ b/test/test_ordered_fold_control.jl @@ -0,0 +1,2 @@ +include(joinpath(@__DIR__, "fixtures", "ordered_fold_control_contracts.jl")) +ordered_fold_control_contracts(Array) From e51eeaf7b27e54d27ee6372e73375a6b4b1a9551 Mon Sep 17 00:00:00 2001 From: PraneethMerugu Date: Tue, 8 Sep 2026 19:15:17 -0400 Subject: [PATCH 4/4] Guard empty pointwise domains before accessing values --- CONTRIBUTING.md | 6 ++ docs/src/api/localmath.md | 6 ++ src/execution/candidate_stage.jl | 69 +++++++++++++-------- test/fixtures/empty_pointwise_contracts.jl | 71 ++++++++++++++++++++++ test/metal/empty_pointwise_domains.jl | 2 + test/metal/runtests.jl | 1 + test/runtests.jl | 1 + test/test_empty_pointwise_domains.jl | 2 + 8 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 test/fixtures/empty_pointwise_contracts.jl create mode 100644 test/metal/empty_pointwise_domains.jl create mode 100644 test/test_empty_pointwise_domains.jl diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1dba1b7..a0496d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,6 +91,12 @@ Ordered-fold participation and publication are owned by retained destinations, and unchanged duplicate-order rejection through the ordinary CPU and Metal inventories. +Pointwise traversal and its control checks are owned by +`src/execution/candidate_stage.jl`. The shared fixture +`test/fixtures/empty_pointwise_contracts.jl` checks empty-domain preparation, +untouched backing storage, nonempty publication, and runtime-prefix diagnostics +through the ordinary CPU and Metal inventories. + Focused commands shorten the edit loop; they are not a second test inventory or release gate. Before handoff, run the complete suite of every changed package. Add the integration suite when a package boundary, extension, SciML diff --git a/docs/src/api/localmath.md b/docs/src/api/localmath.md index b669fa6..4d83ef8 100644 --- a/docs/src/api/localmath.md +++ b/docs/src/api/localmath.md @@ -86,6 +86,12 @@ scratch, and a lifetime wholly contained by one pointwise segment is forwarded without writing that scratch. Ordinary user Fields remain explicitly bound and observable. +An empty pointwise source domain performs no evaluator calls or accesses to +its source and destination elements, including during backend preparation. Its control declarations +still apply: an open gate rejects an invalid runtime prefix, while a closed +gate suppresses that stage. Zero-length field views leave their backing +storage untouched. + An `OrderedFold` stage with a closed `Control` gate does not evaluate or order events, run its recurrence, or publish its initializer over the retained destination. This holds for parameter gates and gates produced by a preceding diff --git a/src/execution/candidate_stage.jl b/src/execution/candidate_stage.jl index 3f5d547..176dbbf 100644 --- a/src/execution/candidate_stage.jl +++ b/src/execution/candidate_stage.jl @@ -258,37 +258,47 @@ end return map(destination -> @inbounds(destination[item]), destinations) end -@inline function _direct_pointwise_member!(qualified, destinations, - empty_policies, materializations, forwarding, cache, predecessors, +@inline function _direct_pointwise_prefix(qualified, predecessors, program_validation, lease_index::Int32, item::Int32) - fields = _pointwise_forward_fields( - qualified.stage.fields, forwarding, cache, item) - stage = _pointwise_stage_with_fields(qualified.stage, fields) - local_qualified = _QualifiedEvaluation(stage, qualified.parameters) - published = _pointwise_current_values(destinations, item) + stage = qualified.stage if _candidate_prefix_succeeded(predecessors, lease_index) && - _stage_gate_open(stage.control.gate, stage, local_qualified.parameters) + _stage_gate_open(stage.control.gate, stage, qualified.parameters) prefix = _stage_prefix_value( - stage.control.prefix, stage, local_qualified.parameters) + stage.control.prefix, stage, qualified.parameters) valid = prefix isa Integer && !(prefix isa Bool) && 0 <= prefix <= stage.source_count if item == 1 && !valid _store_program_validation_status!(program_validation, lease_index, _CANDIDATE_STATUS_INVALID_CONTROL, Int32(0), Int32(0), Int32(0), UInt32(0)) - elseif item <= stage.source_count && valid - active = item <= Int32(prefix) && - _stage_mask_active(stage.control.mask, stage, item) && - _stage_subset_active(stage.control.subset, stage, item) - if active - result = _call_stage_evaluator(local_qualified, item, - _stage_reads(stage, item), local_qualified.parameters) - published = _direct_unique_publish_all!(destinations, result, - empty_policies, materializations, item) - else - published = _direct_unique_publish_all_empty!( - destinations, empty_policies, materializations, item) - end + end + return valid ? Int32(prefix) : Int32(-1) + end + return Int32(-1) +end + +@inline function _direct_pointwise_member!(qualified, destinations, + empty_policies, materializations, forwarding, cache, predecessors, + program_validation, lease_index::Int32, item::Int32) + fields = _pointwise_forward_fields( + qualified.stage.fields, forwarding, cache, item) + stage = _pointwise_stage_with_fields(qualified.stage, fields) + local_qualified = _QualifiedEvaluation(stage, qualified.parameters) + published = _pointwise_current_values(destinations, item) + prefix = _direct_pointwise_prefix(local_qualified, predecessors, + program_validation, lease_index, item) + if prefix >= 0 + active = item <= prefix && + _stage_mask_active(stage.control.mask, stage, item) && + _stage_subset_active(stage.control.subset, stage, item) + if active + result = _call_stage_evaluator(local_qualified, item, + _stage_reads(stage, item), local_qualified.parameters) + published = _direct_unique_publish_all!(destinations, result, + empty_policies, materializations, item) + else + published = _direct_unique_publish_all_empty!( + destinations, empty_policies, materializations, item) end end return published @@ -328,9 +338,18 @@ end forwarding, predecessors, program_validation, lease_index::Int32) raw_item = @index(Global, Linear) item = Int32(raw_item) - _direct_pointwise_members!(qualified, destinations, empty_policies, - materializations, forwarding, (), predecessors, - program_validation, lease_index, item) + # Segment members share one traversal domain. The empty-domain lane still + # validates controls, but must never read a destination or evaluate a RHS. + if item <= first(qualified).stage.source_count + _direct_pointwise_members!(qualified, destinations, empty_policies, + materializations, forwarding, (), predecessors, + program_validation, lease_index, item) + elseif item == 1 + foreach(qualified) do member + _direct_pointwise_prefix(member, predecessors, + program_validation, lease_index, item) + end + end end function _execute_direct_pointwise_segment!( diff --git a/test/fixtures/empty_pointwise_contracts.jl b/test/fixtures/empty_pointwise_contracts.jl new file mode 100644 index 0000000..e665f85 --- /dev/null +++ b/test/fixtures/empty_pointwise_contracts.jl @@ -0,0 +1,71 @@ +using Test +import LocalMath +import KernelAbstractions + +struct EmptyPointwiseDomain end +struct EmptyPointwiseCopy end +@inline (::EmptyPointwiseCopy)(item::Int32, reads, parameters) = + (value = LocalMath.UniqueValue(something(reads[1][1].value) + Int32(1)),) + +function empty_pointwise_contracts(array_type) + return @testset "empty pointwise domains retain storage and control diagnostics" begin + for count in (0, 3), controlled_prefix in (false, true), sequence_length in (1, 2) + space = LocalMath.Space(EmptyPointwiseDomain, count) + input, middle, output = ntuple(_ -> LocalMath.Field(space, Int32), 3) + relation = LocalMath.IdentityRelation(space) + enabled = LocalMath.Parameter(:enabled, Bool) + prefix = LocalMath.Parameter(:prefix, Int32; bounds = (Int32(0), Int32(max(count, 1)))) + parameters = controlled_prefix ? (enabled, prefix) : (enabled,) + control = controlled_prefix ? LocalMath.Control(; gate = enabled, prefix) : LocalMath.Control(; gate = enabled) + function stage(source, destination) + LocalMath.Stage( + space, (value = LocalMath.Access(source, relation; required = true),), + ( + LocalMath.Publication( + (LocalMath.FieldPublication(destination, relation, LocalMath.PublicationValue(:value)),), + LocalMath.Unique(Int32; coverage = LocalMath.PartialCoverage(), onempty = LocalMath.PreserveEmpty()) + ), + ), + LocalMath.Evaluator(EmptyPointwiseCopy(), parameters), control, + LocalMath.SourceOrigin(@__FILE__, @__LINE__; label = :empty_pointwise_copy), + ) + end + # Empty views retain observable guard elements in their backing + # buffers. An out-of-domain write must not corrupt those guards. + input_storage = array_type(Int32[10, 20, 30, 41, 42]) + middle_storage = array_type(fill(Int32(71), 5)) + output_storage = array_type(fill(Int32(91), 5)) + law = sequence_length == 1 ? LocalMath.LocalLaw(stage(input, output)) : + LocalMath.sequence(LocalMath.LocalLaw(stage(input, middle)), LocalMath.LocalLaw(stage(middle, output))) + bindings = (input => view(input_storage, 1:count), output => view(output_storage, 1:count)) + sequence_length == 2 && (bindings = (bindings..., middle => view(middle_storage, 1:count))) + prepared = LocalMath.prepare( + law, bindings...; + backend = KernelAbstractions.get_backend(input_storage) + ) + @test all(segment -> segment.family === :direct_pointwise, LocalMath.inspect(prepared; level = :kernels).physical_segments) + @test Array(middle_storage) == fill(Int32(71), 5) + @test Array(output_storage) == fill(Int32(91), 5) + arguments(value, prefix_value) = controlled_prefix ? (; enabled = value, prefix = Int32(prefix_value)) : (; enabled = value) + wait(LocalMath.execute!(prepared; parameters = arguments(false, max(count, 1)))) + @test Array(output_storage) == fill(Int32(91), 5) + wait(LocalMath.execute!(prepared; parameters = arguments(true, count))) + @test Array(middle_storage)[1:count] == (sequence_length == 1 ? fill(Int32(71), count) : Int32[11, 21, 31][1:count]) + @test Array(output_storage)[1:count] == (Int32[10, 20, 30][1:count] .+ Int32(sequence_length)) + @test Array(middle_storage)[(count + 1):5] == fill(Int32(71), 5 - count) + @test Array(output_storage)[(count + 1):5] == fill(Int32(91), 5 - count) + if iszero(count) && controlled_prefix + failure = try + wait(LocalMath.execute!(prepared; parameters = arguments(true, 1))) + nothing + catch error + error + end + @test failure isa LocalMath.LocalMathValidationError + @test failure.actual.failure_class === :invalid_control + @test Array(middle_storage) == fill(Int32(71), 5) + @test Array(output_storage) == fill(Int32(91), 5) + end + end + end +end diff --git a/test/metal/empty_pointwise_domains.jl b/test/metal/empty_pointwise_domains.jl new file mode 100644 index 0000000..ed27188 --- /dev/null +++ b/test/metal/empty_pointwise_domains.jl @@ -0,0 +1,2 @@ +include(joinpath(@__DIR__, "..", "fixtures", "empty_pointwise_contracts.jl")) +empty_pointwise_contracts(Metal.MtlArray) diff --git a/test/metal/runtests.jl b/test/metal/runtests.jl index cb1129a..fce920c 100644 --- a/test/metal/runtests.jl +++ b/test/metal/runtests.jl @@ -15,6 +15,7 @@ const LOCALMATH_METAL_WITNESSES = ( "destination_grouping.jl", "product_values.jl", "ordered_fold_control.jl", + "empty_pointwise_domains.jl", ) @testset "LocalMath Metal runner inventory" begin diff --git a/test/runtests.jl b/test/runtests.jl index 124712f..e8d0795 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,6 +10,7 @@ const LOCALMATH_INCLUDED_TESTS = ( "test_stage_planning.jl", "test_stage_preparation.jl", "test_direct_pointwise_stage.jl", + "test_empty_pointwise_domains.jl", "test_product_values.jl", "test_unique_stage.jl", "test_stage_program_lifecycle.jl", diff --git a/test/test_empty_pointwise_domains.jl b/test/test_empty_pointwise_domains.jl new file mode 100644 index 0000000..18fb747 --- /dev/null +++ b/test/test_empty_pointwise_domains.jl @@ -0,0 +1,2 @@ +include("fixtures/empty_pointwise_contracts.jl") +empty_pointwise_contracts(Array)