Meow Programming Language v0.21.0: A Member Read as Well as Called
Released 2026-08-19. Reading a member without calling it used to come apart — codegen emitted an unchecked assertion, so a program ended with a Go panic rather than a furball:
nyan swap = strings.new_replacer("cat", "nyan")
nyan speak = swap.replace
interface conversion: meowrt.Value is *meowrt.Opaque, not *meowrt.Kitty
A member read is now a value
A field is what it holds. A method — one groomed on, or one belonging to something from Go — is that method bound to what it was reached through, which is a function like any other:
nab go "strings"
nyan swap = strings.new_replacer("cat", "nyan", "dog", "woof")
nyan speak = swap.replace
nya(speak("the cat")) # the nyan
nya("the cat and the dog" |=| swap.replace) # the nyan and the woof
nya(lick(["cat", "dog"], swap.replace)) # [nyan, woof]
This is what a language of values has in place of a chain of calls. Rather than following one call with another, a member is taken as a function and the value passed into it, which reads left to right and needs nothing to come after a closing bracket. The pipe already carried a member on its right side; what was missing was the member being a value at all.
The same holds for a groomed method:
kitty Cat {
name: string
age: int
}
groom Cat {
meow older(n int) string {
bring self.name + " turns " + to_string(self.age + n)
}
}
nyan c = Cat("nyan", 3)
nyan older = c.older
nya(older(1)) # nyan turns 4
nya(2 |=| c.older) # nyan turns 5
nya(lick([1, 2], c.older)) # [nyan turns 4, nyan turns 5]
A compile error was removed on purpose
Method X.y must be called with () is gone. It was what made a method the one
thing in the language that could not be piped into or handed to lick, and it
held backwards: the better the checker knew the type, the less could be done
with the value.
What that costs is that a forgotten () is now caught only where a type is
expected — which, since member inference gives the method’s own function type
rather than any, is most places:
| written | before | after |
|---|---|---|
"a" + c.show | error | Hiss! Cannot add string and () string |
nyan s string = c.show | error | Hiss! Variable s declared as string but assigned () string |
c.show == "nyan" | error | Hiss! Cannot compare () string and string |
nya(c.show) | error | prints <meow show> |
The three shapes that slip through are the places that take any type at all.
The error messages name the type as () string, so a forgotten () still
reads as one.
Calling a member is inferred on a different path and is untouched, so these still fire where they are written:
c.nosuch Hiss! Cat has no field or method nosuch
c.older("x") Hiss! Argument 1 for Cat.older: expected int but got string
c.older() Hiss! Method Cat.older expects 1 arguments but got 0
A second thing this fixes
A groomed method reached through a value whose type is not known — a paw
parameter, say — was unreachable either way, calling or reading. Both of these
used to say Hiss! Cat has no field show:
nyan f = paw(x) { bring x.show }
nyan g = paw(x) { bring x.show() }
Calling a member now goes through the same lookup as reading one, so the two cannot disagree again.
Both backends answer this identically — the playground’s tree-walking interpreter and the compiled path route through one runtime entry point.
Upgrading
If a program relied on Method X.y must be called with () to catch a
forgotten () in a position that takes any type — inside nya, inside a
litter literal, or through to_string — that mistake now prints
<meow show> instead of failing to compile. Everywhere a type is expected it
is still an error. Nothing else needs changing.
brew upgrade meow
# or
go install github.com/135yshr/meow/cmd/meow@v0.21.0