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:

writtenbeforeafter
"a" + c.showerrorHiss! Cannot add string and () string
nyan s string = c.showerrorHiss! Variable s declared as string but assigned () string
c.show == "nyan"errorHiss! Cannot compare () string and string
nya(c.show)errorprints <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