Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,21 @@ 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.

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
Expand Down
12 changes: 12 additions & 0 deletions docs/src/api/localmath.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ 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
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
Expand Down
6 changes: 6 additions & 0 deletions docs/src/learn/localmath-domain-compiler.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
34 changes: 34 additions & 0 deletions docs/src/learn/localmath-relations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions spec/localmath.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
69 changes: 44 additions & 25 deletions src/execution/candidate_stage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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!(
Expand Down
67 changes: 40 additions & 27 deletions src/execution/mechanism_support.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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) = (
Expand All @@ -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)
Expand Down
Loading