a little project for my own learning
caw SPEC.md
4.3 kB
Markdown
at main

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 #

  1. Small enough to hold in your head. Every feature must justify itself.
  2. Explicit over convenient. Prefer catching a mistake over saving a keystroke.
  3. 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: count and Count are 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/3 will 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.