Meow Programming Language Quick Reference

A complete reference of all keywords, operators, and syntax in the Meow Programming Language.

Keywords

MeowMeaningExample
nyanVariable declarationnyan x = 42
meowFunction definitionmeow add(a int, b int) int { bring a + b }
bringReturn a valuebring x + 1
sniffConditional branch (if)sniff (x > 0) { ... }
scratchElse branch} scratch { ... }
purrLoop (count, range, list, or condition)purr i (10) { ... }, purr (ready) { ... }
boltLeave the loopsniff (found) { bolt }
slinkOn to the next turnsniff (empty) { slink }
pawLambda (anonymous function)paw(x int) { x * 2 }
nyaPrint valuesnya("Hello!")
lickTransform each element in a list (map)lick(nums, paw(x) { x * 2 })
pickySelect elements matching a condition (filter)picky(nums, paw(x) { x > 0 })
curlCombine a list into a single value (reduce)curl(nums, 0, paw(a, x) { a + x })
peekBranch based on a value (pattern match)peek(v) { 0 => "zero", _ => "other" }
hissRaise an errorhiss("something went wrong")
gagCatch errors (try/recover)gag(paw() { risky() })
is_furballCheck if a value is an erroris_furball(result)
nabImport standard library packagenab "http"
flauntExport (reserved)
yarnTrue (boolean literal)nyan ok = yarn
hairballFalse (boolean literal)nyan ng = hairball
catnapNil (represents no value)nyan nothing = catnap
kittyStruct (composite type) definitionkitty Cat { name: string }
breedType alias (transparent)breed Nickname = string
collarNewtype (nominal wrapper)collar UserId = int
poseInterface definitionpose Showable { meow show() string }
groomAdd methods to a typegroom Cat { meow show() string { ... } }
selfInstance reference in methodsself.name
trillPure-function modifier (opt-in purity check)trill meow add(a int, b int) int { bring a + b }

Type Keywords

Meow supports gradual static typing. Type keywords annotate variables, function parameters, and return values.

Annotations are optional on variables, where the type is inferred from the initializer, but required on function signatures: every parameter of a meow function must have a type, and a function containing bring must declare its return type. Omitting either is a compile error:

Hiss! Parameter "a" of function add must have a type annotation, nya~
Hiss! Function add has bring statements but no return type annotation, nya~

Grouped parameters satisfy the requirement without repeating the type: in meow add(a, b int) int, a takes the type of the next parameter that has one.

Lambdas are the exception — paw parameters may be left unannotated, and their result type is inferred.

TypeMeaningExample
int64-bit signed integernyan x int = 42
float64-bit floating-pointnyan pi float = 3.14
stringUTF-8 stringnyan name string = "Nyantyu"
boolBooleannyan ok bool = yarn
furballError valuepaw(err furball) { ... }
litterList of valuesnyan nums litter = [1, 2, 3]

Type Annotation Syntax

Variables:

nyan x int = 42
nyan name string = "Nyantyu"

Function parameters and return type:

meow add(a int, b int) int {
  bring a + b
}

Go-style grouped parameter types — parameters without a type receive the type of the next parameter with a type:

meow add(a, b int) int {
  bring a + b
}
# a and b are both int

Operators

Arithmetic

OperatorMeaningExample
+Addition / string concatenation1 + 2, "a" + "b"
-Subtraction / unary negation5 - 3, -x
*Multiplication2 * 3
/Division10 / 2
%Modulo10 % 3

Comparison

OperatorMeaningExample
==Equalx == 0
!=Not equalx != 0
<Less thanx < 10
>Greater thanx > 0
<=Less than or equalx <= 100
>=Greater than or equalx >= 1

Logical

OperatorMeaningExample
&&Logical AND (short-circuit)x > 0 && x < 10
||Logical OR (short-circuit)x == 0 || x == 1
!Logical NOT!ok

Special

OperatorMeaningExample
|=|Pipe (chain operations)nums |=| lick(double)
~>Error recovery (catch)divide(10, 0) ~> 0
.Member accesscat.name, file.snoop("x")
..Range (inclusive)1..10
=>Match arm separator0 => "zero"
=Bind a name (bindings are immutable)nyan x = 1

Operator Precedence

From lowest to highest:

PrecedenceOperatorsDescription
1 (lowest)~>Error recovery
2||Logical OR
3&&Logical AND
4== !=Equality
5< > <= >=Comparison
6|=|Pipe
7+ -Addition, subtraction
8* / %Multiplication, division, modulo
9! - (unary)Unary operators
10 (highest)() [] .Call, index, member access

Literals

TypeExampleDescription
Integer42Decimal integer
Float3.14Floating-point number
String"Hello, world!"Double-quoted. Escapes: \" \\ \n \t \r
List[1, 2, 3]Ordered collection
Map{"key": "value"}String-keyed dictionary

Delimiters

SymbolMeaningExample
( )Function call / groupingadd(1, 2)
{ }Block / map literalmeow f() { ... }
[ ]List / index access[1, 2, 3], nums[0]
,Separatoradd(a, b)
:Type annotation / map key-valuename: string, {"k": v}

Comments

# Line comment

-~ Block comment
   can span multiple lines ~-

Syntax Examples

Variable Declaration

nyan x = 42
nyan greeting = "Hello!"
nyan pi float = 3.14
nyan cats_are_great = yarn
nyan nothing = catnap

The nyan keyword may be omitted, in which case x = 42 declares x just as nyan x = 42 does.

Bindings are immutable. There is no assignment: a name is bound once, and x = ... against a name already bound in an enclosing scope is a compile error rather than a reassignment.

nyan total = 0
purr i (5) {
  total = total + 1   # Hiss! Variable total is already bound and cannot be
                      # reassigned — bindings are immutable, nya~
}

Accumulate with curl instead of updating a variable in a loop:

nyan total = curl([1, 2, 3, 4, 5], 0, paw(acc, x) { acc + x })
nya(total)   # => 15

An explicit nyan inside an inner scope still shadows deliberately, since that declares a new, separate binding.

Function Definition

meow add(a int, b int) int {
  bring a + b
}

nya(add(1, 2))   # => 3

Pure Functions (Trill)

Prefix a function with trill to opt into a purity check. Inside a trill function, the body may only call:

  • other trill functions, and
  • side-effect-free builtins: arithmetic/comparison operators, len, to_int, to_float, to_string, to_bytes, to_runes, is_furball, head, tail, append, lick, picky, curl, whiff, track, shred, tangle, nibble.

Any other call is a compile error: impure builtins (nya, hiss, gag), imported-package members (e.g. http.pounce(...)), and non-trill user functions are all rejected. Higher-order calls are allowed, but lambda bodies are scanned recursively, so an impure lambda passed to lick is still caught.

trill meow add(a int, b int) int {
  bring a + b
}

A non-trill function referenced as a bare value — bound to a variable, passed as an argument, or returned — is rejected too, so an impure function can’t slip out of a pure body without being called.

Both rules are about the declaration a name reaches. A pure body may hold a pure local of its own where an impure function happens to share the name.

Struct (Kitty) Definition

kitty Cat {
  name: string
  age: int
}

nyan nyantyu = Cat("Nyantyu", 3)
nya(nyantyu)            # => Cat{name: Nyantyu, age: 3}
nya(nyantyu.name)       # => Nyantyu
nya(nyantyu.age)        # => 3

Fields are defined with name: type syntax. Instances are created by calling the type name as a constructor. Fields are accessed with . notation.

Type Alias (Breed)

breed Nickname = string
breed Score = int

nyan name Nickname = "Nyantyu"
nya(name + " chan")   # => Nyantyu chan — fully compatible with string

breed creates a transparent alias. The alias and the original type are interchangeable in all contexts including arithmetic, comparisons, and function calls. breed is erased at compile time — no runtime cost.

Newtype (Collar)

collar UserId = int
collar Email = string

nyan id = UserId(42)
nyan email = Email("nyantyu@meow.cat")

nya(id)           # => UserId{value: 42}
nya(id.value)     # => 42
nya(email.value)  # => nyantyu@meow.cat

collar creates a distinct wrapper type. Values are constructed with TypeName(value) and unwrapped with .value. Different collar types with the same underlying type are not interchangeable:

collar Celsius = int
collar Fahrenheit = int

nyan c = Celsius(100)
nyan f = Fahrenheit(212)
# c != f — different collar types are never equal

Conditionals

sniff (x > 0) {
  nya("positive")
} scratch sniff (x == 0) {
  nya("zero")
} scratch {
  nya("negative")
}

Loops

Count form — iterates from 0 to n-1:

purr i (10) {
  nya(i)
}
# prints 0, 1, 2, ..., 9

Range form — iterates from a to b inclusive:

purr i (1..20) {
  nya(i)
}
# prints 1, 2, 3, ..., 20

Error Handling

Use hiss to raise an error and stop execution. The error message is prefixed with Hiss! automatically.

meow divide(a int, b int) int {
  sniff (b == 0) {
    hiss("division by zero")
  }
  bring a / b
}

nya(divide(10, 0))   # => Hiss! division by zero

Multiple arguments are joined with spaces:

hiss("unexpected value:", x)

Use gag to catch errors. Wrap risky code in a lambda and pass it to gag. If the code panics, gag returns a Furball (error value) instead of crashing. Use is_furball to check if a value is an error.

nyan result = gag(paw() { divide(10, 0) })

sniff (is_furball(result)) {
  nya("caught:", result)
} scratch {
  nya("ok:", result)
}
# => caught: Hiss! division by zero

If the code succeeds, gag returns its result normally:

nyan ok = gag(paw() { divide(10, 2) })
nya(ok)   # => 5

Error Recovery with ~>

The ~> (cat tail arrow) operator provides concise error recovery. If the left-hand expression panics, the fallback on the right is used instead. The ~ resembles a cat’s tail, and > points to the fallback.

nyan val = divide(10, 0) ~> 0
nya(val)   # => 0

When no error occurs, the original result is returned:

nyan val = divide(10, 2) ~> 0
nya(val)   # => 5

The fallback can also be a handler function that receives the error:

nyan val = divide(10, 0) ~> paw(err) { 42 }
nya(val)   # => 42

Lambdas

nyan double = paw(x int) { x * 2 }
nya(double(5))   # => 10

A lambda body may also be a block of statements, so control flow is available inside paw. A trailing expression is the result, just as in the single-expression form, and bring returns from the lambda:

nyan classify = paw(n) {
  sniff (n > 10) { bring "big" } scratch { bring "small" }
}
nya(classify(50))   # => big

nyan area = paw(w, h) {
  nyan a = w * h
  a + 1
}
nya(area(3, 4))     # => 13

List Operations

nyan nums = [1, 2, 3, 4, 5]

lick(nums, paw(x) { x * 2 })           # => [2, 4, 6, 8, 10]
picky(nums, paw(x) { x % 2 == 0 })     # => [2, 4]
curl(nums, 0, paw(acc, x) { acc + x })  # => 15

Map Literals

nyan headers = {
  "Content-Type": "application/json",
  "Authorization": "Bearer token123"
}

Pipe

nyan double = paw(x) { x * 2 }
nums |=| lick(double)

The pipe operator passes the left value as the first argument of the right expression:

[1, 2, 3] |=| lick(paw(x) { x * 2 }) |=| nya
# => [2, 4, 6]

A chain can be written down the page: a line that begins with |=| continues the expression on the line above rather than starting a statement of its own.

[1, 2, 3, 4, 5, 6]
  |=| picky(paw(x) { x % 2 == 0 })
  |=| lick(paw(x) { x * x })
  |=| nya
# => [4, 16, 36]

The break has to come immediately before the |=|. A blank line or a comment between two stages ends the statement, and the |=| after it has nothing on its left.

Pattern Matching

nyan result = peek(score) {
  0 => "zero",
  1..10 => "low",
  11..100 => "high",
  _ => "off the charts"
}

Patterns can be:

  • Literal — match a specific value (0, "hello", yarn)
  • Range — match an inclusive range (1..10)
  • Wildcard — match anything (_)

Import (Nab)

Use nab to import a standard library package:

nab "file"
nab "http"
nab "env"
nab "clock"
nab "json"
nab "random"
nab "testing"

After importing, call package functions with package.function() syntax:

nab "file"
nyan content = file.snoop("data.txt")
nya(content)

Available packages: clock, env, file, http, json, random, testing. See stdlib.md for details.

With go, the string is a Go import path rather than one of Meow’s own packages, and any Go package can be reached without a wrapper written for it:

nab go "strings"
nab go "net/url" tag u                              # name it yourself
nab go "github.com/Masterminds/semver/v3@v3.3.1"    # pin a version

nya(strings.to_upper("nyan"))
nyan parsed = u.parse("https://example.com/a/b")
nya(parsed["host"])

The package is called by the last element of its path, or by tag when that is not a name a program can write. A major-version element is not that name: it belongs to the module, so github.com/Masterminds/semver/v3 is semver. Only v2 and up are version elements — a module at v0 or v1 carries no suffix, which is why k8s.io/api/core/v1 really is called v1. Go names are spelled the way Meow writes names — strings.to_upper is strings.ToUpper — and a name holding an initialism is written as Go writes it, strings.ToValidUTF8. Without @ the version is the toolchain’s choice. See spec.md for what comes back and how it is read.

Member Access

The . operator accesses fields on kitty instances, calls methods defined by groom, and calls functions on imported packages:

# Kitty field access
nyan nyantyu = Cat("Nyantyu", 3)
nya(nyantyu.name)   # => Nyantyu

# Method call (defined by groom)
nya(nyantyu.show())  # => Nyantyu (age 3)

# Package function call
nab "http"
http.pounce("https://example.com") |=| nya

A member may be named after a keyword, since nothing but a member can follow a dot — a Go method named String is written .string().

A member read rather than called is a value of its own. A field is what it holds; a method is that method bound to what it was reached through, which is a function like any other:

nyan tell = nyantyu.show      # not called: the method, bound to nyantyu
nya(tell())                   # => Nyantyu (age 3)

What it was reached through is already settled, so a bound method’s arguments are the ones the method itself declares — not the thing it is called on. A method taking one argument therefore pipes and maps on that argument:

groom Cat {
  meow greets(who string) string { bring self.name + " greets " + who }
}

nya("Kuro" |=| nyantyu.greets)                  # => Nyantyu greets Kuro
nya(lick(["Tama", "Mike"], nyantyu.greets))     # => [Nyantyu greets Tama, Nyantyu greets Mike]

The same holds for a named function, which can be kept, piped into, or mapped over a list:

meow double(n int) int { bring n * 2 }

nyan twice = double
nya(twice(21))                # => 42
nya(3 |=| double)             # => 6
nya(lick([1, 2, 3], double))  # => [2, 4, 6]

A binding takes a name over for as long as it is in scope, whether the name is read or called, so a local of the same name is the one that is reached.

Interface (Pose)

Define an interface with pose — a named set of method signatures:

pose Showable {
    meow show() string
}

A type structurally satisfies a pose if it has all required methods with matching signatures. No explicit declaration is needed (structural typing).

Methods (Groom)

Add methods to a kitty or collar type with groom:

kitty Cat {
  name: string,
  age: int
}

groom Cat {
    meow show() string {
        bring self.name + " (age " + to_string(self.age) + ")"
    }
    meow is_kitten() bool {
        bring self.age < 1
    }
}

nyan c = Cat("Nyantyu", 3)
nya(c.show())       # => Nyantyu (age 3)
nya(c.is_kitten())  # => hairball

self refers to the instance the method is called on. For kitty types, use self.field to access fields. For collar types, use self.value to access the wrapped value:

collar Label = string

groom Label {
    meow display() string {
        bring "[ " + self.value + " ]"
    }
}

nyan tag = Label("important")
nya(tag.display())   # => [ important ]

Testing

Test functions use the test_ prefix and catwalk_ prefix:

nab "testing"

meow test_addition() {
  expect(1 + 1, 2, "basic addition")
}

meow test_string() {
  judge("hello" == "hello")
}

Run tests with meow test:

meow test my_test.nyan

See stdlib.md for judge, expect, refuse, and other testing functions.