Meow Programming Language v0.21.3: Purity Judged by the Declaration a Name Reaches

Released 2026-08-20. A trill function is pure: it must not call a non-pure one. Until this release, the guard judged that by name alone, so a trill body could not call a pure local of its own where an impure top-level function happened to share the name.

meow helper(x int) int {
  nya("side effect")
  bring x + 1
}

trill meow adds(n int) int {
  nyan helper = paw(x) { bring x + 1 }
  bring helper(n)
}
Hiss! pure function adds must not call non-pure function helper, nya~

The local has nothing to do with the top-level helper, but the purity check looked the name up in the table of top-level functions and never asked whether it still reached that declaration here.

Ask what the name reaches

v0.21.2 recorded which declaration each occurrence reaches. Both purity sites — a call and a bare reference — now ask that instead of the name:

meow helper(x int) int {
  nya("side effect")
  bring x + 1
}

trill meow adds(n int) int {
  nyan helper = paw(x) { bring x + 1 }
  bring helper(n)
}

nya(adds(41))     # 42
nya(helper(1))    # side effect
                  # 2

The fault predates v0.21.2; v0.21.2 is what made it fixable.

What the guard is for is untouched

An impure function reached by its own name is still refused, called or merely referenced:

bring impure(n)              Hiss! pure function bad must not call non-pure function impure
bring lick([n], impure)[0]   Hiss! pure function bad must not reference non-pure function impure

A regression caught in review is worth naming, because it made the release better. Gating on the recorded resolution rested on an assumption: that a resolution is recorded wherever the purity walk can see a name. It was not — patterns were never inferred at all, so a name written in a peek arm reached nothing and the gate let it through, turning a clean refusal into a Go build error escaping from codegen:

trill meow bad(n int) int {
  bring peek(n) {
    impure => 1
    _ => 0
  }
}

The fix was to make the inference go where the walk goes — a pattern is an expression, and one naming something is a reference like any other — rather than to make the purity check fall back to the name. A nested meow, a groom block inside a trill body, a sniff condition, a call inside a range pattern, and a name bound to the function and then called were all walked by hand afterwards; all still refuse.

The typed call path was also changed to resolve through the recorded declaration rather than the name table. No program was found that reaches it with a shadowed name — holding a function value pushes the whole body onto the boxed path — so that half is defence in depth rather than a fix for an observed fault.

Upgrading

Nothing to change, and no golden output moved. A trill function that was refused for a local of its own now compiles.

brew upgrade meow
# or
go install github.com/135yshr/meow/cmd/meow@v0.21.3