Cassette is a small functional programming language. It looks like this:

import IO, Net, String

; keeps reading from a connection while there's any data
def read_resp(conn) do
  ; define an iterative loop
  def loop(received) do
    let chunk = IO.read_chunk(conn, 1024) ; get the next chunk

    if #chunk == 0,
      received                            ; no more data
    else
      loop(received <> chunk)             ; concatenate chunk onto received data
  end

  ; start the loop
  loop("")
end

let conn = Net.connect("cassette-lang.com", "80"),  ; open a network connection
    req = String.join([                             ; form an HTTP request
      "GET / HTTP/1.0",
      "Host: cassette-lang.com",
      "",
      ""
    ], "\r\n")

IO.write(conn, req)         ; send the request
IO.print(read_resp(conn))   ; read the response and print it

I made Cassette as a simple language for personal programming. It's DIY, roll your own, batteries-not-included. It's for fun.

Here are some features of Cassette:

Getting Started

To use Cassette, you must build it from source.

  1. Get the project's dependencies
    • On macOS with Homebrew, run brew install llvm git
    • On Debian, run apt install build-essential clang git libx11-dev
  2. Build Cassette
    • Clone Cassette with git clone https://github.com/protestContest/Cassette (and then cd Cassette)
    • Run make to build the project. This creates the executable bin/cassette.
  3. Try the example with ./bin/cassette -L share test/test.ct.

Syntax

Values

Comments
; end of line comment

---
Multiline comment

Comment text must be between triple hyphens.
---

Comments can appear at the end of a line or span multiple lines.

Numbers
10_000            ; decimal integer
0x1F              ; hex integer
$a                ; byte literal (0x61)
true              ; => 1
false             ; => 0

Cassette has only four value types: integers, pairs, tuples, and binaries.

Integers are signed, 30-bit numbers (-536,870,912 to 536,870,911). Integers can be written in decimal, hexadecimal, or a literal byte value. The keyword true is shorthand for 1 and the keyword false is shorthand for 0.

Symbols
:hello
:ok
:not_found_error

Symbols are arbitrary values. (Some languages call them atoms.) At runtime, these become the integer hash value of the symbol name.

Pairs
100 : 200         ; pair
nil               ; empty list
[1, 2, 3]         ; list, same as 1 : 2 : 3 : nil

Pairs are Lisp-style cons cells, which are used to create linked lists. The keyword nil is shorthand for the empty list. The pair operator, :, is right-associative.

Tuples
{1, 2, 3}

Tuples are fixed-size arrays. They're less flexible than lists, but they use less memory and are more efficient to access. The maximum size of a tuple is the maximum integer size.

Strings
"Hello!"

Binaries are byte vectors. Strings are represented as UTF-8 encoded binaries. The maximum size of a binary is the maximum integer size.

Strings support these escape codes:

Operators

Cassette supports several built-in operators on values. Most operators only work on certain types.

Arithmetic
-24               ; negation
73 + 4            ; addition
87 - 41           ; subtraction
43 * 12           ; multiplication
17 / 4            ; division (truncating)
400 % 12          ; modulus
256 >> 3          ; bit shift right (arithmetic)
1 << 27           ; bit shift left
0xAA | 1          ; bitwise or
0xB ^ 0x6         ; bitwise xor
1036 & 0xFF       ; bitwise and
~7                ; bitwise not
12 < 3            ; comparison
12 <= 3
12 > 3
12 >= 3

Basic arithmetic and comparison operators work with integers.

Object Access
; get the head of a pair
@(1 : 2)          ; => 1
@[1, 2, 3]        ; => 1

; get the tail of a pair
^(1 : 2)          ; => 2
^[1, 2, 3]        ; => [2, 3]

; join two tuples or binaries
{1, 2} <> {3, 4}  ; => {1, 2, 3, 4}
"ab" <> "cd"      ; => "abcd"

; get the length of a tuple or binary
#{:foo, :bar}     ; => 2
#"hello"          ; => 5

; get an element of a tuple or binary
{1, 2, 3}[0]      ; => 1
"test"[2]         ; => $s (an integer)

; slice a tuple or binary
{1, 2, 3, 4}[1,3] ; => {2, 3}
"hello"[1,4]      ; => "ell"

Some operators only work with pairs, tuples, or binaries. Joining or slicing tuples and binaries makes a copy of the contents.

Logic
false or :ok      ; => :ok
true and nil      ; => nil
not nil           ; => true
not {0, 0}        ; => false
3 == 3            ; => true
[1, 2] == [1, 2]  ; => true

Logic and equality operators work with any type. Only the values 0 (a.k.a. false) and nil evaluate as false. Logic operators short-circuit and evaluate to one of their operands. Equality is compared structurally, and returns true or false.

Control Structures

Functions
\a, b -> a + b

join([1, 2, 3], ";")

Functions can be created as lambdas. Function calls look similar to other languages.

Conditionals
if true, :ok else :error

if
  x >= 10,  :ten_plus
  x >= 1,   :one_plus
  else      :less_than_one

An if expression is a list of predicate/consequent pairs. It tests each predicate until one is true, then evaluates that predicate's consequent. If none are true, the else expression is evaluated.

Blocks
do
  some_work()   ; executed, but ignored
  other_work()  ; block result
end

A do block is a list of expressions. The result of the last expression is the result of the block.

Def
do
  inf_loop()    ; this is fine since the function is defined for the whole block

  def bar(x)
    {:bar, x}

  def inf_loop()
    inf_loop()
end

In a do block, you can define functions with def. def assigns a function to a variable, which is in scope for the whole block. Functions defined with def can refer to themselves recursively.

Def Guards
def fact(n) when n <= 1,
  1

def fact(n)
  n * fact(n - 1)

A function can be defined multiple times with def, and each can optionally include a guard clause. When called, each guard clause is tested until one is true, then that version of the function is evaluated. Every definition must have the same number of arguments.

Guards
do
  guard foo(100) else "Foo failed!"

  "Foo succeeded"
end

A do block can contain guard statements. A guard statement is similar to an if expression, except that if the condition is false, the block returns early with the guard's alternative value.

Variables

Variables
let x = 3,
    y = x * 2 + 1
x - y

let nums = [1, 2, 3, 4, 5],
    nums = filter(nums, odd?),
    nums = map(nums, \n -> n*2)
done(nums)

A do block can contain let statements. A let statement is a list of assignments. Each assigned variable is in scope in the subsequent assignments (but not in its own) and in the rest of the block.

Records
record Rect(left, top, right, bottom)

let r = Rect(0, 0, 400, 300)
r.right

In a do block, you can define records with record. This is syntactic sugar for defining a function that maps symbol keys to values. Members can be accessed with the . operator. The format of records is undefined.

Modules

Modules
module Foo
import Bar (bar_fn), LongModuleName as LMN
export foo, foo2

def foo(x) do
  let y = Bar.parse(x)
  LMN.run(y)
end

def foo2(x) :unimplemented

bar_fn(x)    ; no qualifier needed

Cassette programs are composed of modules. The body of the module is a do block, and can define functions with def. All top-level def-defined functions are exported, and can be imported in other modules. A module can reference imported functions by qualifying them with the module name or alias. Module declaration, import, and export statements must appear first in a module (its "header").

Primitives
Host.write(file, data)

Built-in functions are executed as functions within the Host module. These transfer control of the VM to a native function. A reference of currently-implemented primitives is available here.