update canvases at 30 fps
add checksum to compiled scripts
add dependency info to readme
Cassette is a small, Lisp-like programming language. It is a home-cooked meal. It looks like this:
import List
import Math
import Canvas
import System
let width = 800,
height = 480,
canvas = Canvas.new(width, height)
canvas.text("Lines!", {200, 2})
System.seed(System.time())
def rand-line(i) do
let x0 = Math.floor(i * width / 100),
y0 = Math.rand-int(20, height / 10),
x1 = Math.rand-int(0, width),
y1 = Math.rand-int(20, height)
canvas.line({x0, y0}, {x1, y1})
end
List.map(\i -> rand-line(i), List.range(0, 100))
I made Cassette as a simple language for "playful programming". Playful programming is writing something for the sake of writing it. It's making a software 3D renderer or a GIF reader, even though better implementations of those already exist. It's making generative art programs and drawing them with a pen plotter. Cassette itself is playful programming—there are certainly other scripting languages that may be better for personal projects like these, but this one is mine.
Here are some of the design goals of Cassette:
In particular, I wanted Cassette to feel "essential", where each aspect of the language reflects some fundamental aspect of computing (from a functional language perspective, at least). For example, I consider garbage collection, lexical scopes, and immutable types essential. The result is a little boring, but I hope it's a good platform to play with other programming concepts.
Here's some future work for Cassette:
This project requires a C build toolchain and SDL2. The source code can be found here.
brew install llvm git sdl2 sdl2_ttf
apt install build-essential clang git libsdl2-dev libsdl2-ttf-dev libfontconfig-dev
git clone https://git.sr.ht/~zjm/Cassette
.make
to build the project. This creates the executable cassette
.make install
to install the Cassette executable. You can set the install folder in the Makefile../cassette test/test.ct
../cassette script.ct
.-1
1 ; decimal integer
0x1F ; hex integer
$a ; => 0x61
1.0 ; float
-4 ; => -4
1 + 2 ; => 3
5 * 5 ; => 25
10 / 2 ; => 5.0
12 % 10 ; => 2
0xF0 >> 4 ; => 0x0F (shift right)
0x55 << 1 ; => 0xAA (shift left)
0x37 & 0x0F ; => 0x07 (bitwise and)
20 ^ 1 ; => 21 (bitwise or)
~4 ; => 3 (bitwise not)
:ok
:not_found
3 > 1 ; => true
3 < 3 ; => false
5 <= 4 + 1 ; => true
5 >= 4 + 1 ; => true
:ok == :ok ; => true
:ok != :ok ; => false
3 > 1 and 4 == 5 ; => false
3 > 1 or 4 == 5 ; => true
3 >= 0 and 3 < 5 ; => true
nil and :ok ; => false
:error and :ok ; => true
"Hello!"
#"Hello!" ; => 6
#"" ; => 0
"Hi " <> "there" ; => "Hi there"
"foo" <> "bar" ; => "foobar"
"ob" in "foobar" ; => true
$r in "foobar" ; => true
65 in "Alpha" ; => true
1000 in "foobar" ; error: bytes must be between 0–255
[1, 2, 3] ; list
[1, 2, 3].2 ; => 3
[1, 2, 3].8 ; error: out of bounds
[:a, x, 42]
nil == [] ; => true
1 | [2, 3, 4] ; => [1, 2, 3, 4]
3 | nil ; => [3]
1 | 2 | 3 | nil ; => [1, 2, 3]
#[1, 2, 3] ; => 3
#nil ; => 0
[1, 2] <> [3, 4] ; => [1, 2, 3, 4]
:ok in [:ok, 3] ; => true
{1, 2, 3} ; tuple
{1, 2, 3}.2 ; => 3
{1, 2, 3}.8 ; error: out of bounds
#{1, 2, 3} ; => 3
#{} ; => 0
{1, 2} <> {3, 4} ; => {1, 2, 3, 4}
"x" in {"y", "z"} ; => false
{x: 3, y: 4} ; a map with keys `:x` and `:y`
my_map.x ; => 3
my_map.z ; => nil
#{x: 1, y: 2} ; => 2
{x: 1, z: 4} <> {x: 2, y: 3}
; => {x: 2, y: 3, z: 4}
:x in {x: 1, y: 2} ; => true
let x = 1, y = 2, x-y = 3
print(x - y) ; prints "-1"
print(x-y) ; prints "3"
print(x -y) ; error: tries to call function "x" with argument "-y"
do
let y = 3, z = 4
x ; => 1 (from the parent scope)
y ; => 3 (shadows the parent `y`)
z ; => 4
end
x ; => 1
y ; => 2
z ; error: undefined variable
if x == 0 do
IO.print("Uh oh!")
:error
else
:ok
end
cond do
x > 10 -> :ten
x > 1 -> :one
true -> :less ; default
end
let foo = \x -> x + 1
foo(3) ; => 4
; equivalent, except for scope:
let foo = \x -> x + 1
def foo(x) x + 1
; these produce an error, since `b` isn't defined when the body of `a` is compiled
let a = \x -> (b x * 3),
b = \x -> (a x / 2)
; these are ok, since `a` and `b` are in scope from the beginning of the block
def a(x) b(x * 3)
def b(x) a(x / 2)
let pi = 3.14 def bar(x) x + 1
; file "main.ct"
import Foo ; imported as a map called Foo
Foo.bar(3) ; => 4 Foo.pi ; => 3.14
; alternative "main.ct"
import Foo as F ; imported as a map called F
def bar(x) x + 8
bar(3) ; => 11 F.bar(3) ; => 4
; alternative "main.ct" import Foo as * ; imported directly into current scope
bar(3) ; => 4 pi ; => 3.14
</div>
</div>
## [More Info](#more-info)
For more information about Cassette, check out some of these other documents. Stay tuned for future articles.
- [Function Reference](https://cassette-lang.com/reference.html): A reference of built-in modules and functions