Skip to content

Raise a fill to a literal power the way Base does - #446

Open
jishnub wants to merge 1 commit into
jishnub/broadcaststylefrom
jishnub/literalpow
Open

Raise a fill to a literal power the way Base does#446
jishnub wants to merge 1 commit into
jishnub/broadcaststylefrom
jishnub/literalpow

Conversation

@jishnub

@jishnub jishnub commented Aug 14, 2026

Copy link
Copy Markdown
Member

Stacked on #385 — please review that first; this PR targets jishnub/broadcaststyle and its diff is a single commit.

x .^ k with a literal k lowers to a Base.literal_pow broadcast, which four styleless rules intercept so a fill raised to a power stays a fill. Each read the answer off the exponent rather than off the value the operation produces, and three were wrong for a negative one:

Zeros{Float64}(3) .^ -1   # Zeros [0.0,0.0,0.0]   Base: [Inf, Inf, Inf]
Zeros{Int}(3)     .^ -1   # Zeros{Int} [0,0,0]    Base: [Inf, Inf, Inf]
Fill(2,3)         .^ -1   # DomainError           Base: [0.5, 0.5, 0.5]
Ones{Int}(3)      .^ -1   # Ones{Int} [1,1,1]     Base: Float64 [1.0,1.0,1.0]

Causes: the AbstractZeros/Val{k} rule asserted 0^k == 0 for every k != 0; the AbstractOnes rule took the element type from its argument rather than from the result, which inv on an integer promotes; and the generic rule evaluated getindex_value(r)^k, which is undefined where literal_pow is defined (2^-1 throws, literal_pow(^, 2, Val(-1)) is 0.5).

There was also an internal inconsistency: written as a literal, Zeros{Float64}(3) .^ -1 returned zeros, but with the exponent in a variable — which does not lower to literal_pow, and so takes the generic path through _copy_fill — the same expression already returned Inf.

All of this reproduces on master; it is pre-existing rather than a regression from #385, but it sits in code that branch rewrites.

This restores the strong-zero convention rather than departing from it

Zeros is deliberately an absorbing zero, but only as a factor. The rule loop generates exactly (*, Zeros, T), (/, Zeros, T), (*, T, Zeros) and (\, T, Zeros) — every shape where the zero is a factor or a numerator — and fillalgebra.jl does the same for matrix products. It deliberately does not absorb in a denominator: there is no (/, T, Zeros) rule, so x ./ Zeros falls through to IEEE and gives Inf, and where both sides are Zeros, / and \ are routed through _broadcasted_nan to construct Fill(NaN) — the more expensive answer, pinned by a testset named "NaN".

A negative power is a division with the zero in the denominator, so it belongs in the second group. Three existing behaviours already said so:

  • inv.(Zeros(3)) already returned Fill(Inf),
  • Fill(0.0,3) .^ -1 already returned Fill(Inf) while Zeros(3) .^ -1 returned zeros — two spellings of the same array disagreeing,
  • Twos ./ Zeros === Fill(Inf, ...) is already asserted in the test suite.

Absorption is unchanged wherever the zero is a factor. (The "avoid ∞ * 0 in InfiniteArrays.jl" comment is not counter-evidence: it sits on _foldl_length_op guarding length(A)*v, so that is an infinite length, not an infinite value.)

Scope of the behaviour change

Measured over Zeros/Ones in Int, Float64, Float32, Bool, ComplexF64 and Fill of 0, 1, 2, 2.0, against exponents -3..3:

  • every changed result has k < 0; nothing at k >= 0 changes,
  • every changed result moves from disagreeing with the dense array to agreeing with it, in value and element type,
  • no test used a negative exponent, so nothing pinned the old behaviour.

Implementation

Classifying by the computed value is what _copy_fill already does, so the four rules collapse into one that shares it through a new _fill_result:

broadcasted(op::typeof(Base.literal_pow), x::typeof(^), r::AbstractFill, y::Val) =
    _fill_result(op, op(x, getindex_value(r), y), axes(r), r)

Note for downstream packages

A package overloading only broadcasted_zeros/broadcasted_ones now falls back to Fill for Zeros .^ negative, since the result is neither zero nor one and only broadcasted_fill is left to reach. Recorded in the tests as TaggedZeros{Int}(4) .^ -1 ≡ Fill(Inf, 4).

Verification

  • Test.detect_ambiguities(FillArrays; recursive=false) → 0
  • A 14.6k-line behavioural snapshot over unary/binary/fused broadcasts, powers, operators and resolved styles, diffed against the parent commit: 78 changed lines, all of them negative exponents, nothing else
  • Full test suite green, including the customisation hooks, the DefaultArrayStyle-forwarded path and the infinite-axes cases

🤖 Generated with Claude Code

`x .^ k` with a literal `k` lowers to `literal_pow`, which four styleless rules
intercepted. Each read the answer off the exponent rather than off the value the
operation produces, and three were wrong for a negative one:

    Zeros{Float64}(3) .^ -1  ->  Zeros [0,0,0]     Base: [Inf, Inf, Inf]
    Fill(2,3)         .^ -1  ->  DomainError       Base: [0.5, 0.5, 0.5]
    Ones{Int}(3)      .^ -1  ->  Ones{Int}         Base: Float64 ones

The rule for a `Zeros` asserted `0^k == 0` for every `k != 0`; the one for a `Ones`
took the element type from its argument rather than from the result, which `inv` on
an integer promotes; and the generic one evaluated `getindex_value(r)^k`, which is
undefined where `literal_pow` is not.

A negative power is a division, and the zero sits in the denominator. The package
already declines to absorb there: no rule intercepts `x ./ Zeros`, and two `Zeros`
under `/` or `\` are sent through `_broadcasted_nan` on purpose. Two spellings of the
same array disagreed, `Fill(0.0,3) .^ -1` giving `Inf` where `Zeros(3) .^ -1` gave
zero, and `inv.(Zeros(3))` already gave `Inf`. Absorption is unchanged where the zero
is a factor.

Classifying by the value is what `_copy_fill` already does, so the four rules collapse
into one that shares it through `_fill_result`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.84%. Comparing base (aef27ef) to head (96d4cf9).

Additional details and impacted files
@@                    Coverage Diff                     @@
##           jishnub/broadcaststyle     #446      +/-   ##
==========================================================
- Coverage                   99.84%   99.84%   -0.01%     
==========================================================
  Files                           9        9              
  Lines                        1275     1272       -3     
==========================================================
- Hits                         1273     1270       -3     
  Misses                          2        2              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants