Caw — language specification, v0.1 #
Caw is a small, interpreted programming language. It is named after the sound
a crow makes. Source files end in .caw.
This document is the agreement between the person writing Caw code and the program that runs it. If the interpreter disagrees with this document, one of the two is wrong and we have to decide which.
Design principles #
- Small enough to hold in your head. Every feature must justify itself.
- Explicit over convenient. Prefer catching a mistake over saving a keystroke.
- Readable errors. A confusing error message is a bug.
Lexical structure #
- Comments start with
//and run to the end of the line. Discarded entirely. - Whitespace (spaces, tabs) is insignificant except as a separator between tokens.
- Newlines end a statement. There are no semicolons.
- Identifiers start with a letter or
_, then any letters, digits or_. Case-sensitive:countandCountare different variables. - Numbers are decimal, with an optional fractional part:
42,3.14. Internally all numbers are floating point. (See "Known limitations".) - Strings are double-quoted:
"hello". Escapes:\n,\t,\",\\.
Keywords #
Reserved, and therefore unusable as variable names:
let fn return if else while true false nil and or not
Types #
| Type | Examples | Notes |
|---|---|---|
| number | 42, 3.14, -7 |
all numbers are floating point |
| string | "caw" |
immutable |
| boolean | true, false |
|
| nil | nil |
the absence of a value |
| function | fn (x) { ... } |
created with fn |
Truthiness #
Only false and nil are falsy. Everything else — including 0 and "" —
is truthy. This is a deliberate choice: it means if x asks "does x exist?"
rather than "is x non-zero?", which is the question people usually mean.
Operators, loosest to tightest binding #
or
and
== !=
< > <= >=
+ -
* /
not - (prefix, applied to one value)
() (function call)
+ adds numbers and concatenates strings. Mixing a number and a string is an
error rather than a silent conversion — silent conversion is a famous source of
bugs in other languages.
Statements #
let NAME = EXPRESSION // declare a new variable
NAME = EXPRESSION // reassign an existing variable
if EXPRESSION { ... } else { ... }
while EXPRESSION { ... }
fn NAME(A, B) { ... } // declare a function
return EXPRESSION
EXPRESSION // evaluated, result discarded
let declares; bare = reassigns. Reassigning a variable that was never
declared is an error. This catches typos that would otherwise silently create
a second variable.
Scope #
Each { ... } block creates a new scope. Names declared inside a block are
invisible outside it. An inner scope can see and modify names from enclosing
scopes.
Built-in functions #
| Name | Purpose |
|---|---|
print(x) |
write x followed by a newline to output |
len(x) |
length of a string |
str(x) |
convert any value to a string |
num(x) |
convert a string to a number, or error |
Example program #
// Count crows.
let crows = 3
fn describe(n) {
if n == 1 {
return "one crow"
} else {
return str(n) + " crows"
}
}
while crows > 0 {
print(describe(crows))
crows = crows - 1
}
print("a murder of nothing")
Expected output:
3 crows
2 crows
one crow
a murder of nothing
Known limitations (v0.1) #
Deliberately out of scope for now. Listed so they're decisions, not oversights:
- No integers, only floats.
1/3will look ugly. - No lists, no maps, no objects.
- No modules or imports — one file at a time.
- No closures capturing state after the enclosing function returns.
- Very slow. It's an interpreter written in an interpreted language.