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:
- Functional
- Immutable values
- Garbage collection
- Efficient tail-call recursion
- Modules
Getting Started
To use Cassette, you must build it from source.
- 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
- On macOS with Homebrew, run
- Build Cassette
- Clone Cassette with
git clone https://github.com/protestContest/Cassette
(and thencd Cassette
) - Run
make
to build the project. This creates the executablebin/cassette
.
- Clone Cassette with
- Try the example with
./bin/cassette -L share test/test.ct
.
Syntax
Comments
Comments can appear at the end of a line or span multiple lines.
; end of line comment --- Multiline comment Comment text must be between triple hyphens. ---
Values
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
.
10_000 ; decimal integer 0x1F ; hex integer $a ; byte literal (0x61) true ; => 1 false ; => 0
Symbols are arbitrary values. (Some languages call them atoms.) At runtime, these become the integer hash value of the symbol name.
:hello :ok :not_found_error
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.
100 : 200 ; pair nil ; empty list [1, 2, 3] ; list, same as 1 : 2 : 3 : nil
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.
{1, 2, 3}
Binaries are byte vectors. Strings are represented as UTF-8 encoded binaries. The maximum size of a binary is the maximum integer size.
"Hello!"
Strings support these escape codes:
\a
: ASCII bell (0x07)\b
: Backspace (0x08)\e
: Escape (0x1B)\f
: Form feed (0x0C)\n
: New line (0x0A)\r
: Carriage return (0x0D)\s
: Space (0x20)\t
: Tab (0x09)\v
: Vertical tab (0x0B)\0
: Null (0x00)- Any other character prefixed with
\
: That character
Operators
Cassette supports several built-in operators on values. Most operators only work on certain types.
Basic arithmetic and comparison operators work with integers.
-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
Some operators only work with pairs, tuples, or binaries. Joining or slicing tuples and binaries makes a copy of the contents.
; 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"
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
.
false or :ok ; => :ok true and nil ; => nil not nil ; => true not {0, 0} ; => false 3 == 3 ; => true [1, 2] == [1, 2] ; => true
Functions
Functions can be created as lambdas. Function calls look similar to other languages.
\a, b -> a + b join([1, 2, 3], ";")
Conditionals
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.
if true, :ok else :error if x >= 10, :ten_plus x >= 1, :one_plus else :less_than_one
Blocks
A do
block is a list of expressions. The result of the last expression is the result of the block.
do some_work() ; executed, but ignored other_work() ; block result 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.
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
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.
def fact(n) when n <= 1, 1 def fact(n) n * fact(n - 1)
Guard
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.
do guard foo(100) else "Foo failed!" "Foo succeeded" end
Variables
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.
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)
Records
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.
record Rect(left, top, right, bottom) let r = Rect(0, 0, 400, 300) r.right
Modules
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").
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
Primitives
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.
Host.write(file, data)