Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hello!

Crash-tan, saluting with a wink: purple hair, an orange hoodie with her name on it, and teal trainers.

Hi, I’m Crash-tan — the reality-bending android behind Crash BASIC!

When you write a program, I’m the one who reads it and makes it real. You say PRINT and words appear. You say there’s a dragon with 500 hp, and now there is one. Helping you cause the best kind of chaos for your games is my favorite job!

I’m also the one who tells you when something’s gone sideways. I’ll be showing you around.

You already know how to program. Maybe it was another BASIC, maybe Python, JavaScript or Lua. So I won’t explain what a variable is. I’ll show you how I do things, and point out where that’s different from what you’re used to.

How this page works

The lesson is on this side. On the other side is an editor with real code in it, and under that, whatever the code printed. It’s all real: that’s me over there, running in your browser.

Every piece of code in a lesson has a Try it button. Press it and the code goes into the editor and runs. Then change it! Press Run (or Ctrl + Enter) to run it again, and Reset when you’d like the lesson’s version back.

PRINT "Hello! I'm Crash-tan."
PRINT "2 + 2 is", 2 + 2
Hello! I'm Crash-tan.
2 + 2 is 4

PRINT takes as many values as you like, separated by commas, and puts a space between them.

Break something

Go on — change PRINT to PRNT over there and run it. I’d rather you met my error messages now, while nothing’s at stake. I always try to say what I was looking at and what to try next.

PRNT "this won't work"

The small print

Three things to know before we start.

// A comment starts with two slashes and runs to the end of the line.
print "capitals don't matter to me"   // PRINT, Print and print are one word
Score = 10
PRINT score, SCORE                    // ...and so are names
capitals don't matter to me
10 10
  • Comments are //.
  • Capitals don’t matter, in my words or in your names. I’ll write my own words in capitals in these lessons so you can tell them from yours.
  • One statement to a line. There’s no ; at the end and no : to squeeze two onto one line.

From BASIC: A comment starts with // where you’d write ' or REM. There are no line numbers and no : between statements.

From Python: Indent however you like. I go by the line that ends a block, like END IF.

From JavaScript: No semicolons, no braces around blocks. Braces are for maps (your objects) and nothing else.

From Lua: Comments start with // where you’d write --, and a block ends by naming what it closes: END IF, END FUNCTION.

One more thing, and it’s the big one. I have very few words of my own. PRINT isn’t one of them! Words like PRINT and FOR are given to me by whatever program I’m running inside — here, this tutorial. Later on I’ll show you how to make words of your own. That’s my favorite part.

Values

Here’s everything a value can be.

lives = 3                 // a whole number
ratio = 2.5               // a number with a fraction
mask = 0xFF               // hex; 0b1010 is binary
name = "Ada"              // text
ready = TRUE              // TRUE is 1 and FALSE is 0
nothing = NULL            // no value at all
PRINT lives, ratio, mask, name, ready, nothing
3 2.5 255 Ada 1 NULL

A variable is made by giving it a value. There’s no DIM, let, var or local to write first.

Any name you like

I come with a lot of built-in things, and whatever program I’m running inside adds more. You don’t have to learn their names before you choose yours. If you make a variable with the same name as one of them, yours wins.

len = 12                  // LEN is one of mine. In this program, it's yours
PRINT len + 1
13

Yours wins everywhere in your program: in every file, inside every function, even on lines above the one that makes it. That means the built-in is out of reach for as long as your variable exists. In the program above, LEN("four") wouldn’t count letters any more. It would try to look inside the number 12. If you want the built-in back, give your variable another name.

Two kinds of name stay taken:

  • A keyword’s words. PRINT = 3 can’t be a variable, because I couldn’t tell it from the statement.
  • Your own functions and types. If your program declares FUNCTION score(), then score = 3 is a mistake, and I’ll say so.

From BASIC: No $ or % on the end of a name. name$ is just name, and a variable can hold a different kind of value later on.

From Python: TRUE, FALSE and NULL are your True, False and None. TRUE really is the number 1.

From JavaScript: There’s one “nothing”, NULL. No undefined.

From Lua: NULL is your nil.

Text

Text goes in double quotes. + joins it to anything, and * repeats it.

name = "Ada"
PRINT "Hello, " + name + "!"
PRINT "lives: " + 3
PRINT "ab" * 3
PRINT "She said \"hi\"\tand left."
PRINT LEN(name), UPPER(name), LOWER(name)
Hello, Ada!
lives: 3
ababab
She said "hi"	and left.
3 ADA ada

You read text the way you’ll read a list: a position in parentheses, counting from 0. One position gives one character. A range gives a run of them, both ends included.

name = "Crash-tan"
PRINT name(0), name(5)
PRINT name(0 ... 4)
PRINT name(8 ... 0)           // a range that counts down reads backwards
C -
Crash
nat-hsarC

From BASIC: MID$, LEFT$ and RIGHT$ are a range in parentheses now: LEFT$(name, 5) is name(0 ... 4).

From Python and JavaScript: name[0:5] and name.slice(0, 5) are name(0 ... 4): the end is included.

From Lua: name:sub(1, 5) is name(0 ... 4): counting starts at 0.

When you need text that runs over several lines, or one full of quotes, put it between backticks. I take it exactly as written — nothing is escaped. If it starts on the line after the backtick, I drop the indent the lines share.

poem = `
    Roses are "red",
      violets are \blue\,
    this text is exactly as you see it.
    `
PRINT poem
Roses are "red",
  violets are \blue\,
this text is exactly as you see it.

From Python: Backticks are your triple quotes. Single quotes don’t make a string here.

From JavaScript: Backticks give you the many lines of a template literal, but there’s no ${} inside one. Join with +.

From Lua: Backticks are your [[long strings]], and + is your ...

Two values you might not have met

A coordinate is two numbers that travel together, and a vector is the trip from one coordinate to another. I’m made for games, so I have them built in.

spot = (10, 20)
PRINT spot, spot.x, spot.y
PRINT spot + (1, 1), spot * 2

trip = (0, 0) -> (30, 40)
PRINT trip, trip.start, trip.stop.y
(10, 20) 10 20
(11, 21) (20, 40)
(0, 0) -> (30, 40) (0, 0) 40

A range is every whole number from one end to the other — both ends included. You’ll use them in loops.

PRINT 1 ... 5
PRINT 1 TO 5          // the same thing, the way BASIC writes it
1 ... 5
1 ... 5

From Python: 1 ... 5 includes the 5. range(1, 5) wouldn’t.

Values that never change

CONST gives a variable its value once. If any line tries to give it another, I’ll refuse to run the program at all. Better that than have it go wrong later.

CONST max_lives = 5
PRINT "you get", max_lives, "lives"
you get 5 lives
CONST max_lives = 5
max_lives = 99

Crash-tan’s Tip: Notice I caught that before running a single line. I read the whole program first and check everything I can. If it starts, the names and the words in it are all real.

Sums and comparisons

Arithmetic

PRINT 7 + 2, 7 - 2, 7 * 2
PRINT 7 / 2           // dividing always gives a fraction
PRINT 7 \ 2           // a backslash divides and keeps the whole part
PRINT 7 MOD 2         // what's left over
PRINT 2 ^ 10          // to the power of
PRINT SQRT(16), INT(3.9), ABS(-5), MIN(4, 2, 8), MAX(4, 2, 8)
9 5 14
3.5
3
1
1024
4.0 3 5 2 8

From BASIC: These are the ones you know: \, MOD and ^.

From Python: \ is your //, MOD is your %, and ^ is your **.

From JavaScript: ^ is a power here, and your XOR is ~. MOD is your %.

From Lua: \ is your // and MOD is your %.

Comparing

One = does both jobs. At the start of a line it gives a variable a value. Anywhere else it asks whether two things are equal.

score = 10
PRINT score = 10, score <> 10
PRINT score > 5, score >= 10, score < 5, score <= 9
PRINT "apple" < "banana"
1 0
1 1 0 0
1

A comparison gives 1 for yes and 0 for no.

From Python, JavaScript and Lua: There’s no ==. Equal is = and not equal is <>.

AND, OR, NOT

They’re words. 0 and NULL count as no; other numbers count as yes.

lives = 3
shield = FALSE
PRINT lives > 0 AND NOT shield
PRINT lives = 0 OR shield
PRINT NOT NULL
1
0
1

From BASIC: AND, OR and NOT work on yes and no here. The bit operators are below.

From JavaScript: AND and OR give 1 or 0. They don’t hand back one of their two values, so name OR "nobody" won’t give you a default.

From Lua: The same goes for you: x OR y gives 1 or 0.

Bits

When you need them, they’re symbols: & and, | or, ~ exclusive or (and, with one value, not), << and >> to shift.

flags = 0b1010
PRINT flags & 0b0110, flags | 1, flags ~ 0b1111
PRINT 1 << 4, 256 >> 2, ~0
2 11 5
16 64 -1

Making decisions

IF

A block IF has no THEN. It ends with END IF.

hp = 35

IF hp > 50
    PRINT "fine"
ELSE IF hp > 0
    PRINT "hurt"
ELSE
    PRINT "down"
END IF
hurt

THEN is for when the whole thing fits on one line. That form has no END, and it can have an ELSE on the end — even ELSE IF, if you can stand the length.

hp = 0
IF hp = 0 THEN PRINT "game over"
IF hp > 0 THEN PRINT "you won't see this"

IF hp > 50 THEN PRINT "fine" ELSE PRINT "not fine"
IF hp > 50 THEN mood = "happy" ELSE IF hp > 0 THEN mood = "worried" ELSE mood = "napping"
PRINT mood
game over
not fine
napping

Each side takes one statement. When you need two, that’s what the block form is for.

From BASIC: A block IF doesn’t end its first line with THEN, and it’s ELSE IF, two words. (If you’ve got a lot of old code, the last lesson shows a way to let me read it as it is.)

From Python: ELSE IF is your elif. No colons.

From JavaScript: No parentheses needed around the condition, and no braces around the block.

From Lua: ELSE IF is your elseif, and there’s no then on a block.

SELECT CASE

When you’re asking what one value is, SELECT CASE reads better than a ladder of ELSE IF. I work the value out once, then run the lines under the first CASE that fits — only that one.

score = 95

SELECT CASE score
    CASE 100
        PRINT "perfect!"
    CASE 90 TO 99
        PRINT "great"
    CASE 50, 60, 70, 80
        PRINT "a round number"
    CASE IS < 50
        PRINT "oops"
    CASE ELSE
        PRINT "fine"
END SELECT
great

A CASE can list values with commas, give a range with TO, or compare with IS. CASE ELSE catches everything and goes last. Change score over there and see which one I pick.

From Python: This is your match, for values.

From JavaScript: This is your switch, without the break. A case never falls through into the next.

From Lua: You never had one of these. Enjoy!

Going round again

FOR

FOR steps through whatever you give it. Give it a range and it counts.

FOR i = 1 ... 3
    PRINT "lap", i
END FOR
lap 1
lap 2
lap 3

A range goes whichever way gets it from its start to its end, so counting down needs nothing special. To take bigger steps, add !! step(n).

That !! is the WOMT operator: Wait! One more thing! It’s for when a statement was all finished and then you remembered something else it needed. What comes after it is a modifier — an extra, named setting on a keyword. You’ll be making your own later.

FOR i = 3 ... 1
    PRINT i
END FOR
PRINT "lift off!"

FOR i = 0 ... 20 !! step(5)
    PRINT "tick", i
END FOR
3
2
1
lift off!
tick 0
tick 5
tick 10
tick 15
tick 20

Give FOR a list and it hands you each item. Ask for two names and you get the position as well. A map gives its keys and values.

FOR fruit = ["apple", "pear"]
    PRINT fruit
END FOR

FOR place, fruit = ["apple", "pear"]
    PRINT place, fruit
END FOR

FOR name, hp = {ada: 10, grace: 30}
    PRINT name, "has", hp
END FOR
apple
pear
0 apple
1 pear
ada has 10
grace has 30

NEXT closes a FOR just as END FOR does. Use whichever you like.

FOR i = 1 TO 3
    PRINT i
NEXT i
1
2
3

From BASIC: FOR i = 1 TO 10 ... NEXT i works as written. STEP is the modifier !! step(2).

From Python: FOR item = list is your for item in list, and the two-name form is your enumerate.

From JavaScript: One FOR does the work of for, for...of and for...in.

From Lua: No pairs or ipairs: hand FOR the list or the map itself. And positions start at 0 here.

WHILE and DO

WHILE tests before each pass. DO runs first and can test afterwards, with LOOP WHILE or LOOP UNTIL.

fuel = 3
WHILE fuel > 0
    PRINT "burning", fuel
    fuel = fuel - 1
END WHILE

DO
    fuel = fuel + 2
LOOP UNTIL fuel >= 5
PRINT "refuelled to", fuel
burning 3
burning 2
burning 1
refuelled to 6

WEND closes a WHILE too, and DO WHILE and DO UNTIL put the test at the top, if that’s how your fingers remember it.

BREAK and CONTINUE

BREAK leaves the loop. CONTINUE skips to its next pass.

FOR n = 1 ... 10
    IF n MOD 2 = 0 THEN CONTINUE
    IF n > 7 THEN BREAK
    PRINT n
END FOR
1
3
5
7

Crash-tan’s Tip: Try writing a loop that never ends over there, like WHILE TRUE with nothing to stop it. I’ll let it run for a bit and then step in, so you can’t get stuck.

Lists and maps

Lists

A list (I call it an array) goes in square brackets. You read an item with parentheses, and positions start at 0.

scores = [10, 20, 30]
PRINT scores, LEN(scores)
PRINT scores(0), scores(2)
scores(1) = 25
PRINT scores
[10, 20, 30] 3
10 30
[10, 25, 30]

Yes, parentheses: scores(0). Square brackets only ever make a list.

Reaching past the end is an error:

scores = [10, 20, 30]
PRINT scores(3)

+ and - give you a new list. PUSH, POP, QUEUE and DEQUEUE change the list itself.

scores = [10, 20]
PRINT scores + [30]        // joined on
PRINT scores + 1           // one more empty place
PRINT scores               // ...and scores is as it was

PUSH scores, 30            // onto the end
PRINT scores
PRINT POP(scores), scores  // off the end

line = ["ada", "grace"]
QUEUE line, "linus"        // onto the end
PRINT DEQUEUE(line), line  // off the front
[10, 20, 30]
[10, 20, 0]
[10, 20]
[10, 20, 30]
30 [10, 20]
ada [grace, linus]

A piece of a list

Give a range where the position goes and you get a new list of those items — both ends included, as ranges always are. A range that counts down reads backwards.

scores = [10, 20, 30, 40, 50]
PRINT scores(1 ... 3)
PRINT scores(0 ... 1), scores(4 ... 0)

top = scores(3 ... 4)
PUSH top, 60               // a new list: scores isn't touched
PRINT top, scores
[20, 30, 40]
[10, 20] [50, 40, 30, 20, 10]
[40, 50, 60] [10, 20, 30, 40, 50]

Both ends have to be there. Asking for scores(3 ... 9) is an error, just as scores(9) would be.

Lists in lists

Lists can hold lists. Reach in with one pair of parentheses.

grid = [[1, 2, 3], [4, 5, 6]]
PRINT grid(1, 2), grid(0)
6 [1, 2, 3]

Grids of numbers

A list of rows of numbers is a matrix. I’m built for games, and matrices are how games turn, stretch and move things — so they get three operators of their own: @*, @+ and @-. (Plain + on two lists joins them, remember. The @ says “do the math instead”.)

a = [[1, 2], [3, 4]]
b = [[5, 6], [7, 8]]
PRINT a @* b                // each row on the left meets each column on the right
PRINT a @+ b, b @- a        // part by part
PRINT a @* 2                // a number scales every part
PRINT a + b                 // ...and plain + still joins
[[19, 22], [43, 50]]
[[6, 8], [10, 12]] [[4, 4], [4, 4]]
[[2, 4], [6, 8]]
[[1, 2], [3, 4], [5, 6], [7, 8]]

Multiply a matrix by a coordinate and the coordinate goes through it. This one is a quarter turn:

turn = [[0, -1], [1, 0]]
ship = (3, 4)
PRINT turn @* ship
PRINT turn @* turn @* ship           // two quarter turns: facing the other way
PRINT turn @* ((0, 0) -> (1, 0))     // a vector has both ends turned
(-4, 3)
(-3, -4)
(0, 0) -> (0, 1)

A matrix can’t move a point of its own size — that takes one more row and column. So when the matrix is one bigger than the coordinate, I treat the coordinate as having a 1 on the end, and take it off again afterwards. One 3 by 3 matrix can then turn, stretch and move a 2D point all at once.

// Twice the size, then 10 across and 20 down.
place = [[2, 0, 10],
         [0, 2, 20],
         [0, 0, 1]]

FOR corner = [(0, 0), (1, 0), (1, 1), (0, 1)]
    PRINT corner, "goes to", place @* corner
END FOR
(0, 0) goes to (10, 20)
(1, 0) goes to (12, 20)
(1, 1) goes to (12, 22)
(0, 1) goes to (10, 22)

The sizes have to meet. If the left one isn’t as many across as the right one is down, I’ll stop and tell you both numbers.

PRINT [[1, 2], [3, 4]] @* [[1, 2, 3]]

From Python: @* is NumPy’s @, with nothing to install, and @+ and @- are what + and - do on its arrays.

From JavaScript and Lua: No library and no nested loops. It’s built in.

From BASIC: MAT C = A * B, if you go back that far!

You can also ask for a list of a size, every item starting as its type starts. A coordinate for the size gives you rows.

slots = 3:INTEGER
names = 2:STRING
board = (2, 3):INTEGER
board(1, 2) = 7
PRINT slots, names, board
[0, 0, 0] [, ] [[0, 0, 0], [0, 0, 7]]

From BASIC: DIM scores(10) AS INTEGER is scores = 10:INTEGER, and that’s 10 items, numbered 0 to 9.

From Python: scores[1:4] is scores(1 ... 3): the end is included. There are no negative positions, and a slice past the end is an error.

From JavaScript: Reading past the end is an error, where you’d get undefined. scores.slice(1, 4) is scores(1 ... 3).

From Lua: Positions start at 0. I know. I’m sorry.

Maps

A map goes in braces: names and their values. It’s JSON, so you can paste JSON straight in.

player = {
    name: "Ada",
    hp: 100,
    items: ["sword", "map"],
    home: {x: 3, y: 4},
}
PRINT player.name, player.items(0), player.home.x
player.hp = player.hp - 10
player.level = 2                    // a new key is made by giving it a value
PRINT player
PRINT player.missing                // a key that isn't there is NULL
Ada sword 3
{home: {x: 3, y: 4}, hp: 90, items: [sword, map], level: 2, name: Ada}
NULL

Read a key with a dot when you know its name, and with parentheses when the name is in a variable or isn’t a plain word.

key = "hp"
stats = {hp: 100, "top speed": 12, (key + "_max"): 150}
PRINT stats(key), stats("top speed"), stats.hp_max
100 12 150

From Python: A bare key is its own name: {hp: 100} has the key "hp". For a key from a variable, wrap it in parentheses: {(key): 100}.

From JavaScript: Just like your object literals, with (key) where you’d write [key].

From Lua: {hp = 100} is {hp: 100}. Your one table is two things here: a list and a map.

One list, many names

A list or a map is one thing with possibly many names. Give it to another variable and both names mean the same list.

mine = [1, 2]
yours = mine
PUSH yours, 3
PRINT mine                 // the same list, so it changed too

copy = mine + []           // + always makes a new one
PUSH copy, 4
PRINT mine, copy
[1, 2, 3]
[1, 2, 3] [1, 2, 3, 4]

Numbers, text and coordinates are plain values: each variable has its own.

Functions

FUNCTION area(width, height)
    RETURN width * height
END FUNCTION

PRINT area(3, 4)
12

A function can be used above the line it’s written on. I read the whole program before running any of it, so the order doesn’t matter.

Defaults, and as many as you like

A parameter can have a value to fall back on. And a last parameter with ... after it collects everything else into a list.

FUNCTION greet(who, greeting = "Hello", mark = "!")
    RETURN greeting + ", " + who + mark
END FUNCTION

FUNCTION total(first, rest...)
    LOCAL sum = first             // LOCAL: more on that in a moment
    FOR value = rest
        sum = sum + value
    END FOR
    RETURN sum
END FUNCTION

PRINT greet("Ada")
PRINT greet("Bob", "Howdy")
PRINT total(1, 2, 3, 4)
Hello, Ada!
Howdy, Bob!
10

** goes the other way: it spreads a list out into arguments in order, or a map into arguments by name.

FUNCTION greet(who, greeting = "Hello", mark = "!")
    RETURN greeting + ", " + who + mark
END FUNCTION

words = ["Cy", "Welcome"]
options = {mark: "?", greeting: "Is that you"}
PRINT greet(**words)
PRINT greet("Di", **options)
Welcome, Cy!
Is that you, Di?

From Python: rest... is your *args, and ** spreads both lists and maps.

From JavaScript: rest... is your ...rest, and ** is your spread.

From Lua: rest... is your ..., already packed into a list.

Whose variable is it?

This one matters, so slow down. A variable you give a value to is shared by the whole program — even if the line is inside a function. LOCAL makes one that belongs to the function alone.

score = 0

FUNCTION win(points)
    LOCAL bonus = points * 2     // only exists in here
    score = score + bonus        // the program's score
END FUNCTION

win(5)
win(1)
PRINT score
12

Parameters are the function’s own, like a LOCAL.

From BASIC: Just like you’re used to, except it’s LOCAL for the function’s own.

From Python: The other way round from you! No global needed to change a shared variable — and you need LOCAL to not share one.

From JavaScript: LOCAL is your let. Leaving it off is like assigning to a global.

From Lua: Exactly your rule: shared unless you say LOCAL.

A function is a keyword already

A line can call a function without the parentheses. That’s all PRINT is doing.

FUNCTION cheer(who, times = 1)
    PRINT "Go " + who + "!" * times
END FUNCTION

cheer("Ada", 2)
cheer "Bob", 3
Go Ada!!
Go Bob!!!

Telling people about it

A comment with three slashes above a function is its documentation. Editors show it when someone hovers over the name, and I can turn a whole program’s worth into a reference page.

/// How much room a rectangle takes up.
/// !returns: the width times the height
/// !example: PRINT area(3, 4)
FUNCTION area(width, height)
    RETURN width * height
END FUNCTION

PRINT area(5, 5)
25

Types

A map is fine until you want every monster to have the same fields. A TYPE says what the fields are and what each starts as.

TYPE Monster
    name AS STRING = "slime"
    hp AS INTEGER = 20
    pos AS COORDINATE
    loot AS ARRAY
END TYPE

blob = Monster()                              // everything as it starts
boss = Monster({name: "dragon", hp: 500})     // some of it filled in
boss.pos.x = 12
PUSH boss.loot, "gold"
PRINT blob
PRINT boss.name, boss.hp, boss.pos, boss.loot
Monster {name: slime, hp: 20, pos: (0, 0), loot: []}
dragon 500 (12, 0) [gold]

You make one by calling the type like a function, with a map of whatever you want to set. A field with no value of its own starts as its kind does: 0, an empty string, (0, 0), an empty list.

Functions that belong to a type

Write the type’s name, ::, and the function’s. Inside, SELF is the one it was called on.

TYPE Monster
    name AS STRING = "slime"
    hp AS INTEGER = 20
END TYPE

FUNCTION Monster::Hit(damage)
    SELF.hp = SELF.hp - damage
    RETURN SELF.hp > 0
END FUNCTION

FUNCTION Monster::Describe()
    RETURN SELF.name + " (" + SELF.hp + " hp)"
END FUNCTION

blob = Monster()
PRINT blob.Describe()
still_up = blob.Hit(15)
PRINT blob.Describe(), still_up
slime (20 hp)
slime (5 hp) 1

Types inside types

A field can be another type. Give it a map and I’ll turn the map into that type for you, filling in the rest.

TYPE Weapon
    name AS STRING = "fists"
    power AS INTEGER = 1
END TYPE

TYPE Hero
    name AS STRING
    weapon AS Weapon
END TYPE

ada = Hero({name: "Ada", weapon: {name: "sword", power: 7}})
bob = Hero({name: "Bob"})
PRINT ada.name, ada.weapon.name, ada.weapon.power
PRINT bob.name, bob.weapon.name, bob.weapon.power
Ada sword 7
Bob fists 1

From BASIC: TYPE ... END TYPE as you know it, plus defaults and functions of its own.

From Python: A TYPE is a small class: fields with defaults, and methods. SELF is self, and you don’t list it as a parameter. There’s no inheritance.

From JavaScript: SELF is this, without the surprises. There are no prototypes and no new.

From Lua: No metatables to set up. thing.Hit(5) passes SELF for you, so there’s no : to remember.

Crash-tan’s Tip: The AS STRING part tells readers — and the documentation — what a field is for. I don’t police it while the program runs.

Namespaces and files

NAMESPACE

A namespace is a named place to keep things, so that two parts of a program can each have a start without colliding.

NAMESPACE Audio
    volume = 7

    FUNCTION louder()
        volume = volume + 1
        RETURN volume
    END FUNCTION
END NAMESPACE

NAMESPACE Video
    FUNCTION louder()
        RETURN "that's not my job"
    END FUNCTION
END NAMESPACE

PRINT Audio.louder(), Audio.volume
PRINT Video.louder()
8 8
that's not my job

From outside you reach in with a dot. From inside, the namespace’s own things need no prefix. Types and constants live in them too — a namespace of constants makes a tidy set of named numbers.

NAMESPACE Mode
    CONST idle = 0
    CONST playing = 1
    CONST paused = 2
END NAMESPACE

now = Mode.playing
IF now = Mode.playing THEN PRINT "the game is on"
the game is on

ENUM

A namespace of constants that just count upward is so common that there’s a shorter way to write it. An ENUM numbers its members for you: the first is 0, and each one after is one more than the one before.

ENUM Mode
    idle
    playing
    paused
END ENUM

PRINT Mode.idle, Mode.playing, Mode.paused

now = Mode.paused
IF now = Mode.paused THEN PRINT "press any key"
0 1 2
press any key

Give a member a value and the counting carries on from there. You can put several on a line, and a value can use the members before it — handy for flags that get combined.

ENUM Tile
    grass, sand, water          // 0, 1, 2
    wall = 100, door, window    // 100, 101, 102
END ENUM

ENUM Flag
    solid = 1, sticky = 2, deadly = 4
    lava = sticky | deadly
END ENUM

PRINT Tile.water, Tile.door, Tile.window
PRINT Flag.lava, Flag.lava & Flag.deadly
2 101 102
6 4

An ENUM is a namespace underneath, so everything you know about those applies: its members can’t be changed, and it can live inside another namespace.

From BASIC: BEGIN ENUM is ENUM, and the members are reached through its name: Mode.idle.

From Python: An Enum whose members are plain numbers. Mode.idle is just 0.

From JavaScript: The frozen object of numbers you’ve written by hand a hundred times.

From Lua: The table of named numbers, without the table.

From Python and Lua: A namespace is like a module, but you can have several in one file, or one spread over several files.

From JavaScript: Think of an object that only holds declarations, checked before the program runs.

IMPORT

IMPORT brings in another file. This tutorial keeps one called shapes for you to try it on. It holds a namespace, Shapes, with area, edge and a constant, sides.

IMPORT "shapes"

PRINT Shapes.area(3, 4)
PRINT Shapes.edge(3, 4)
PRINT "a rectangle has", Shapes.sides, "sides"
12
14
a rectangle has 4 sides

The name is the file’s path without its .crash. Everything a file declares at its top level is yours once you’ve imported it, and a file imported twice is only read once.

Crash-tan’s Tip: Put a library’s things in a namespace named after it. Then whoever imports it can see at a glance where Shapes.area came from.

Chains

Here’s something you probably haven’t had before. ## passes a value along to the next step, and _ is the value that arrived.

FUNCTION double(n)
    RETURN n * 2
END FUNCTION

FUNCTION shout(text)
    RETURN UPPER(text) + "!"
END FUNCTION

result = 4 ## double(_) ## "the answer is " + _ ## shout(_)
PRINT result
THE ANSWER IS 8!

Read it left to right: start with 4, double it, put words in front, shout it. Without the chain you’d write it inside out: shout("the answer is " + double(4)).

A chain can run over several lines, one step to a line. A statement can be nothing but a chain, too, as long as it starts by calling something.

FUNCTION only_big(list, least)
    LOCAL kept = []
    FOR item = list
        IF item >= least THEN PUSH kept, item
    END FOR
    RETURN kept
END FUNCTION

FUNCTION total(list)
    LOCAL sum = 0
    FOR item = list
        sum = sum + item
    END FOR
    RETURN sum
END FUNCTION

scores = [5, 12, 8, 30]

only_big(scores, 10)
    ## total(_)
    ## PRINT "big ones add up to", _
big ones add up to 42

Notice that _ can go anywhere in the step: first argument, second, in the middle of a sum. Any function will do, just as it was written.

Spreading the value out

So far _ has gone into the next step as one argument. Often the value coming along is really several: a pair of numbers, a set of options. That’s what the spread operator, **, is for. You met it in Functions; it earns its keep here.

**_ hands a list to the next step as separate arguments, in order.

FUNCTION area(width, height)
    RETURN width * height
END FUNCTION

FUNCTION size_of(room)
    RETURN [room.wide, room.deep]
END FUNCTION

kitchen = {wide: 3, deep: 4}

floor = kitchen ## size_of(_) ## area(**_)
PRINT floor
12

size_of gave back one list, and area wanted two numbers. **_ is the join between them, so neither function had to know about the other.

A map spreads too, into arguments by name. That makes a chain a tidy way to build up options and hand them over.

FUNCTION greet(who, greeting = "Hello", mark = "!")
    RETURN greeting + ", " + who + mark
END FUNCTION

FUNCTION excited(options)
    options.mark = "!!!"
    RETURN options
END FUNCTION

said = {greeting: "Welcome"} ## excited(_) ## greet("Ada", **_)
PRINT said
Welcome, Ada!!!

You can mix the two in one step: ordinary arguments, _ as it is, and **_ spread out.

FUNCTION total(first, rest...)
    LOCAL sum = first
    FOR value = rest
        sum = sum + value
    END FOR
    RETURN sum
END FUNCTION

sum = [1, 2, 3] ## total(100, **_)
count = [1, 2, 3] ## LEN(_)
PRINT sum, count
106 3

From Python: No more reading f(g(h(x))) from the inside out.

From JavaScript: Like method chaining with ., except the next step can be any function at all, and you choose where the value goes.

From Lua: Same idea as : chaining, without needing a method for it.

From Python: **_ is your *args and **kwargs at the call, in one operator: a list spreads in order and a map spreads by name.

From JavaScript: **_ is ...value for a list. For a map it fills parameters by name, which a spread object can’t do for you.

Crash-tan’s Tip: Every step after the first has to use _ somewhere, spread or whole. If one doesn’t, the value would be thrown away, and I’ll say so before the program runs.

Functions as values

A function is a value like any other. You can put it in a variable, hand it to another function, or keep it in a map.

Writing one where you need it

=> makes a function out of one expression: what goes in on the left, what comes back on the right.

square = n => n * n
add = (a, b) => a + b
PRINT square(7), add(2, 3)
49 5

Mostly you’ll write them right where another function wants one.

FUNCTION keep(list, wanted)
    LOCAL kept = []
    FOR item = list
        IF wanted(item) THEN PUSH kept, item
    END FOR
    RETURN kept
END FUNCTION

FUNCTION change(list, how)
    LOCAL changed = []
    FOR item = list
        PUSH changed, how(item)
    END FOR
    RETURN changed
END FUNCTION

numbers = [1, 2, 3, 4, 5, 6]
PRINT keep(numbers, n => n MOD 2 = 0)
PRINT change(numbers, n => n * 10)

keep(numbers, n => n > 2)
    ## change(_, n => n * n)
    ## PRINT _
[2, 4, 6]
[10, 20, 30, 40, 50, 60]
[9, 16, 25, 36]

When one line isn’t enough

A FUNCTION with no name is the long form. It ends with END FUNCTION like any other.

FUNCTION change(list, how)
    LOCAL changed = []
    FOR item = list
        PUSH changed, how(item)
    END FOR
    RETURN changed
END FUNCTION

labels = change([3, 50, 700], FUNCTION(n)
    IF n < 10 THEN RETURN "small"
    IF n < 100 THEN RETURN "medium"
    RETURN "huge"
END FUNCTION)
PRINT labels
[small, medium, huge]

What it remembers

A function written inside another keeps a copy of the locals around it, as they were when it was made.

FUNCTION multiplier(factor)
    RETURN n => n * factor
END FUNCTION

double = multiplier(2)
triple = multiplier(3)
PRINT double(10), triple(10)

tools = {twice: double, thrice: triple}
PRINT tools.thrice(5)
20 30
15

From Python: n => n * 2 is your lambda n: n * 2, and the long form is a def you didn’t have to name.

From JavaScript: The same arrows you know. One difference: a function gets a copy of the locals around it. Changing its copy doesn’t change the original, so there’s no counter hiding in a closure. Use a map or a shared variable for that.

From Lua: Same as your function(n) ... end, with the copying rule above where you have upvalues.

From BASIC: This is the thing you used to need GOSUB tricks or a string with a function’s name in it to fake.

Your own keywords

Here’s the part I promised. PRINT is a function that this tutorial bound to a word. You can do exactly the same.

BIND

BIND gives a function a keyword of one to four words. A keyword is called without parentheses, at the start of a line.

FUNCTION say(text, ending = ".")
    PRINT text + ending
END FUNCTION

BIND SAY say
BIND SAY HELLO say("Hello there")

SAY "Keywords are just functions"
SAY HELLO
Keywords are just functions.
Hello there.

The second BIND pins a value to the first parameter, so SAY HELLO needs nothing after it. When a line could start with either keyword, I take the longer one.

Modifiers

Remember FOR i = 0 ... 20 !! step(5), and the WOMT operator — Wait! One more thing!? The part after !! is a modifier: a function that returns a map of parameter names and values, which I fill in for you. ALLOW says which ones a keyword takes.

FUNCTION say(text, ending = ".", times = 1)
    PRINT (text + ending + " ") * times
END FUNCTION

FUNCTION loudly()
    RETURN {ending: "!!!"}
END FUNCTION

FUNCTION again(count)
    RETURN {times: count}
END FUNCTION

BIND SAY say ALLOW(loudly, again)

SAY "hello"
SAY "hello" !! loudly
SAY "hello" !! again(3) loudly
hello. 
hello!!! 
hello!!! hello!!! hello!!! 

Modifiers let a statement read like a sentence with options on the end, without a pile of positional arguments nobody can remember.

Those two modifiers are plain functions, though, which means nobody else can have a variable called again. Put them in a namespace and ALLOW the whole namespace: the short names then only mean something after !!.

FUNCTION say(text, ending = ".", times = 1)
    PRINT (text + ending + " ") * times
END FUNCTION

NAMESPACE Saying
    FUNCTION loudly()
        RETURN {ending: "!!!"}
    END FUNCTION

    FUNCTION again(count)
        RETURN {times: count}
    END FUNCTION
END NAMESPACE

BIND SAY say ALLOW(Saying)

again = 2                         // free to be a variable
SAY "hello" !! again(again) loudly
hello!!! hello!!! 

Modifiers on a function

A function can take modifiers too. Put ALLOW on the end of its FUNCTION line, and a call of it takes them after !!, just as a keyword does. That’s how you ask for a value with options.

NAMESPACE Brewing
    FUNCTION strong()
        RETURN {scoops: 3}
    END FUNCTION

    FUNCTION iced()
        RETURN {ice: TRUE}
    END FUNCTION
END NAMESPACE

FUNCTION coffee(size, scoops = 1, ice = FALSE) ALLOW(Brewing)
    LOCAL made = size + ", " + scoops + " scoops"
    IF ice THEN made = made + ", iced"
    RETURN made
END FUNCTION

PRINT coffee("small")
cup = coffee("large") !! strong iced
PRINT cup
small, 1 scoops
large, 3 scoops, iced

The modifiers go with the call that starts the line, or the one right after the =. In PRINT coffee("large") !! strong, the modifier would be PRINT’s, and PRINT doesn’t know it.

Sometimes a function can’t list its modifiers in advance, because the words depend on something only it knows. Name one of its parameters in ALLOW with ... after it, and every other word after !! lands in that parameter as a map.

FUNCTION order(dish, extras = NULL) ALLOW(extras...)
    RETURN dish + " with " + extras
END FUNCTION

lunch = order("noodles") !! chili(2) egg
PRINT lunch
noodles with {chili: 2, egg: 1}

I can’t check those words for you before the program runs, so use this only when you have to.

A function that reads like a variable

Mark a function !value in its doc comment and it’s read without parentheses, worked out afresh each time.

lives = 3

/// Whether the game is still going.
/// !value
FUNCTION alive()
    RETURN lives > 0
END FUNCTION

PRINT alive
lives = 0
PRINT alive
1
0

From BASIC: This is how things like TIMER and PI are made now, and how every statement you remember — PRINT, LINE, PLAY — gets its word.

From Python, JavaScript and Lua: There’s nothing quite like this where you came from. The nearest is a function call without the brackets — but these are checked before the program runs, show up in the editor’s suggestions, and document themselves.

Crash-tan’s Tip: A keyword’s word is taken for the whole program. Once SAY is bound, a line can’t begin say = 3. Choose words you won’t want for variables.

Keywords with blocks

A keyword can own a block of lines too. There are four kinds, and they differ in one thing: what happens to the block.

BLOCKBIND: the block runs once for each item

The function returns a list (or a range, or a map), and the block runs for each thing in it. This is all FOR is.

FUNCTION countdown(from)
    LOCAL steps = []
    WHILE from > 0
        PUSH steps, from
        from = from - 1
    END WHILE
    RETURN steps
END FUNCTION

BLOCKBIND COUNTDOWN countdown

COUNTDOWN n = 3
    PRINT n, "..."
END COUNTDOWN
PRINT "lift off!"
3 ...
2 ...
1 ...
lift off!

The name before = is the block’s own variable, just as in a FOR. BREAK, CONTINUE and NEXT all work in it.

SCOPEBIND: the block runs once, with something around it

One function runs before the block and makes a value. The function named after END is handed that value when the block is over. In between, anything called inside can ask for it with SCOPE.

FUNCTION list_open(title)
    RETURN {title: title, lines: []}
END FUNCTION

FUNCTION list_add(text)
    PUSH SCOPE("CHECKLIST").lines, text
END FUNCTION

FUNCTION list_close(list)
    PRINT list.title + ":"
    FOR line = list.lines
        PRINT "  [ ] " + line
    END FOR
END FUNCTION

SCOPEBIND CHECKLIST list_open END list_close
BIND ITEM list_add

CHECKLIST "Before launch"
    ITEM "write the docs"
    ITEM "feed the cat"
    IF 2 + 2 = 4 THEN ITEM "tell everyone"
END CHECKLIST
Before launch:
  [ ] write the docs
  [ ] feed the cat
  [ ] tell everyone

Look at what the last four lines turned into: a little language for checklists, with ordinary code allowed in the middle of it. This is how you describe a menu, a level or a song in a script and still have loops and IF to hand.

Keywords that belong to a block

ITEM above works anywhere, which is a waste of a good word: it only means something inside a CHECKLIST. Bind it inside a NAMESPACE and it’s a keyword only where an ALLOW brings it in. That way two blocks can each have an ITEM of their own.

FUNCTION list_open(title)
    RETURN {title: title, lines: []}
END FUNCTION

FUNCTION list_close(list)
    PRINT list.title + ":"
    FOR line = list.lines
        PRINT "  " + line
    END FOR
END FUNCTION

NAMESPACE Checking
    FUNCTION add(text)
        PUSH SCOPE("CHECKLIST").lines, "[ ] " + text
    END FUNCTION
    BIND ITEM add
END NAMESPACE

NAMESPACE Cooking
    FUNCTION add(amount, what)
        PUSH SCOPE("RECIPE").lines, amount + " " + what
    END FUNCTION
    BIND ITEM add
END NAMESPACE

SCOPEBIND CHECKLIST list_open END list_close ALLOW(Checking)
SCOPEBIND RECIPE list_open END list_close ALLOW(Cooking)

FUNCTION chores() ALLOW(Checking)
    ITEM "feed the cat"
    ITEM "water the plants"
END FUNCTION

CHECKLIST "Before launch"
    ITEM "write the docs"
    chores()
END CHECKLIST

RECIPE "Pancakes"
    ITEM 2, "eggs"
    ITEM "1 cup", "flour"
END RECIPE

item = "a variable, out here"
PRINT item
Before launch:
  [ ] write the docs
  [ ] feed the cat
  [ ] water the plants
Pancakes:
  2 eggs
  1 cup flour
a variable, out here

I work out which ITEM a line means from where it’s written, before the program runs. Inside a block, the innermost block wins. A function’s body gets what its own FUNCTION line allows, since it can be called from anywhere. A function you write in place, in the middle of a block, gets what the block allows, the same way it sees the variables around it. Write ITEM somewhere nothing allows it and I’ll say so straight away.

To allow just one keyword out of a namespace, name it with its namespace: ALLOW(Checking.ITEM).

HANDLERBIND: the block is kept for later

The block is kept. It’s handed to the function as a function, to call whenever it likes. GIVES names what the block will be told each time.

listeners = {}

FUNCTION listen(event, handler)
    listeners(event) = handler
END FUNCTION

FUNCTION announce(event, amount)
    LOCAL handler = listeners(event)
    IF handler = NULL THEN RETURN NULL
    RETURN handler(amount)
END FUNCTION

HANDLERBIND WHEN listen GIVES(amount)

WHEN "coin"
    PRINT "picked up a coin worth", amount
    RETURN amount * 2
END WHEN

PRINT "nothing has happened yet"
PRINT "banked", announce("coin", 5)
PRINT "banked", announce("gem", 1)
nothing has happened yet
picked up a coin worth 5
banked 10
banked NULL

TEXTBIND: the block isn’t code at all

Sometimes the lines inside a block aren’t mine to read: a level drawn in characters, a database query, a shader. With TEXTBIND I don’t look at them. I hand them to the function as one piece of text, exactly as you wrote them.

levels = {}

FUNCTION keep_level(name, rows)
    levels(name) = rows
END FUNCTION

TEXTBIND LEVEL keep_level AS "map"

LEVEL "cave"
    #####
    #@.E#
    #####
END LEVEL

PRINT LEN(levels("cave"))
PRINT levels("cave")(6 ... 10)
17
#@.E#
  • The text arrives as the function’s last parameter. Anything written after the keyword fills the parameters before it, as usual.
  • It’s moved left as far as its least indented line, so you can indent the block to match your program.
  • Only END and the keyword’s own words close it. An END IF in the middle is just more text.
  • AS "map" says what kind of text it is. I don’t use that myself. An editor can, to color the text the right way.

Put the TEXTBIND line above the first block that uses it, or in a file you import.

From JavaScript: WHEN "coin" ... END WHEN is on("coin", (amount) => { ... }) with the punctuation taken away.

From Python: A SCOPEBIND is a with block you can build in three lines, and a HANDLERBIND is a decorator that takes a block. A TEXTBIND block is a triple-quoted string that knows which function it’s for.

From Lua: You’d pass a function to get these. Here the block is the function, and the keyword says so.

From BASIC: ON ... GOSUB, WITH and every special block statement you remember are made this way now, by whoever needs one — including you.

When things go wrong

How I think about it

There are two kinds of “wrong”, and I treat them completely differently.

A mistake is fatal — and avoidable. Calling something that doesn’t exist, reaching past the end of a list, dividing by zero: these mean the program itself is wrong. I stop, right there, and say why. I don’t offer a way to carry on regardless, because there’s nothing sensible to carry on to. But every one of them can be avoided by the program: check the length first, check for zero first, spell the name right.

Everything else is a result. The player typed a name nobody has. The save file isn’t there. The shop is out of potions. That’s the world being the world, and a good program expects it. So a function returns those, the same way it returns anything else, and whoever called it looks at what came back.

That’s why I have no try, no catch, no ON ERROR. There isn’t a special second way for things to come out of a function. There’s one way, and the “it didn’t work out” answers travel along it like every other answer.

The rest of this lesson is those two ideas, one at a time.

Mistakes I catch before running

Most mistakes never get as far as running. I read everything first.

FUNCTION area(width, height)
    RETURN width * height
END FUNCTION

PRINT "this line never runs"
PRINT aera(3, 4)

Nothing was printed, not even the first line, because aera isn’t anything. Fix the spelling over there and try again.

Mistakes I can only catch while running

Some can’t be seen until the numbers are real. These stop the program where they happen.

scores = [10, 20, 30]
PRINT "about to go wrong"
PRINT scores(3)
PRINT "this never prints"

Fatal, yes — but look how easy it was to avoid. The program knew how long the list was all along. It just didn’t ask.

scores = [10, 20, 30]
wanted = 3

IF wanted < LEN(scores)
    PRINT scores(wanted)
ELSE
    PRINT "there's no score number", wanted
END IF

parts = 0
IF parts = 0 THEN PRINT "nothing to share out" ELSE PRINT 100 / parts
there's no score number 3
nothing to share out

That’s the deal with mistakes: I won’t let one slide, and you never have to make one.

Results that say “it didn’t work out”

Now the other kind. A function can hand back an error instead of an answer. First it has to declare the error, in its doc comment: a name, what I should say, and optionally a tip. Words in {braces} are filled in when it happens.

/// Finds a player's score.
/// !player-unknown: Nobody called `{name}` is playing!
/// !tip: The players are ada and grace.
FUNCTION score_of(player)
    LOCAL scores = {ada: 12, grace: 30}
    IF scores(player) = NULL THEN RETURN ERROR("player-unknown", {name: player})
    RETURN scores(player)
END FUNCTION

PRINT score_of("ada")

found = score_of("linus")
IF ISERROR(found)
    PRINT "no such player, carrying on"
ELSE
    PRINT found
END IF
12
no such player, carrying on

An error is a value. You can hold it, test it with ISERROR, and decide what to do. All is well: a question was asked, and “nobody by that name” was the honest answer.

A result nobody looked at becomes a mistake

Here’s where the two ideas meet. If an error is used as if it were an ordinary value — printed, added to, passed along — then the program stops, with the message its function declared.

/// Finds a player's score.
/// !player-unknown: Nobody called `{name}` is playing!
/// !tip: The players are ada and grace.
FUNCTION score_of(player)
    LOCAL scores = {ada: 12, grace: 30}
    IF scores(player) = NULL THEN RETURN ERROR("player-unknown", {name: player})
    RETURN scores(player)
END FUNCTION

PRINT score_of("linus") + 10

Not looking at a result that could be “it didn’t work out” is a mistake in the program, so it’s treated like one: fatal, and avoidable. The fix is the ISERROR check from the example before.

So you check the results you can do something about, and anything you didn’t check stops the program by itself, with a proper explanation, at the first place it mattered. Nothing is ever quietly swallowed, and there’s no try to wrap around things.

From Python and JavaScript: No exceptions flying up the stack. An error comes back from the call that made it, and it’s yours to look at. Ignore it and it stops the program at the first place it’s used.

From Lua: Like returning nil, err, except you can’t forget to check: the error is the value.

From BASIC: No ON ERROR GOTO. The check goes right where the thing might fail.

Crash-tan’s Tip: Write the message for the person who’ll be reading it at the worst possible moment. Say what you were looking at, then what to try. That’s what I do!

Several things at once

ASYNC

Put ASYNC in front of a call and it starts on a thread of its own. You get a future back straight away and carry on. RESOLVE waits for a future and gives you its answer.

FUNCTION slow_square(n)
    FOR i = 1 ... 2000
        // pretend this is hard work
    END FOR
    RETURN n * n
END FUNCTION

first = ASYNC slow_square(3)
second = ASYNC slow_square(4)
PRINT "both are working..."
PRINT RESOLVE(first), RESOLVE(second)
PRINT RESOLVE(first, second)      // several at once come back as a list
both are working...
9 16
[9, 16]

Any function can be started this way, and any line can wait for one.

ATOMIC

Threads share the program’s variables. When several are changing the same one, mark it ATOMIC and each change happens as a single step, so none get lost.

ATOMIC counted = 0

FUNCTION count(times)
    FOR i = 1 ... times
        counted = counted + 1
    END FOR
END FUNCTION

RESOLVE(ASYNC count(500), ASYNC count(500), ASYNC count(500))
PRINT counted
1500

From JavaScript: ASYNC f() gives you something like a promise and RESOLVE is await — but it works anywhere, in any function.

From Python: Like submitting to a thread pool and calling .result(), with nothing to set up.

From Lua: Not coroutines: nothing has to yield. I share the time out myself.

From BASIC: No timers and flags to juggle. Start it, carry on, and ask for the answer when you want it.

Crash-tan’s Tip: A program I’m running inside can also start a call later, or again and again — that’s how timers are made, with modifiers: ASYNC blink() !! every(500). This tutorial doesn’t offer those, but a game engine will.

Who cleans up?

I do! You never free anything. There’s no ERASE, no delete, no free. But it’s worth knowing how I decide something is finished with, because it tells you how long your things last.

Things last as long as something holds them

A list, a map or an object stays around for as long as anything is holding it: a variable, another list, a field of an object, a function that kept it. The very moment the last holder lets go, it’s gone, and its memory with it.

TYPE Monster
    name AS STRING
END TYPE

horde = []

FUNCTION spawn(name)
    LOCAL fresh = Monster({name: name})
    PUSH horde, fresh              // the horde holds it now
END FUNCTION                       // `fresh` lets go here; the horde hasn't

spawn("slime")
spawn("bat")
PRINT LEN(horde), horde(0).name

gone = DEQUEUE(horde)              // out of the horde, into `gone`
PRINT LEN(horde), gone.name
gone = NULL                        // the last holder lets go: the slime is freed
2 slime
1 slime

That’s the whole rule. To keep something, put it somewhere: a variable, a list. To let it go, take it out, or give the variable something else. Numbers, text and coordinates are simpler still: each variable has its own copy, so there’s nothing to think about for those.

A local variable lets go when its function finishes. That’s why a function can make a thousand little lists to do its work and none of them pile up.

FUNCTION busy()
    FOR i = 1 ... 1000
        LOCAL scrap = [i, i * 2, i * 3]     // made, used, and freed each pass
    END FOR
    RETURN "all tidy"
END FUNCTION

PRINT busy()
all tidy

Things that hold each other

Here’s the one tricky case, and I handle it too. Sometimes two things hold each other. The player’s target is the enemy, and the enemy’s target is the player. Even when you’ve let go of both, each still has a holder: the other one.

TYPE Fighter
    name AS STRING
    target AS ANY
END TYPE

FUNCTION duel()
    LOCAL ada = Fighter({name: "Ada"})
    LOCAL bug = Fighter({name: "Bug"})
    ada.target = bug
    bug.target = ada               // a ring: each holds the other
    RETURN ada.target.target.name
END FUNCTION                       // nobody outside can reach either one now

PRINT duel()
Ada

Counting holders can’t free those two, since neither count ever gets to nothing. So every so often I stop and look for rings like this: groups of things that are held only by each other, that no variable and no running function can get to any more. Those I take apart and free.

You don’t have to break the ring yourself, or avoid making one. A child that remembers its parent, a list whose items point back at it, a map of handlers that mention the map: go ahead.

What this means for you

  • Nothing to free, ever. Let go of things by not holding them.
  • Ordinary things are freed right away. Memory stays flat while you play; there’s no clean-up to wait for.
  • Rings are freed a little later, the next time I look. How often depends on how much you’re making.
  • I never take anything you can still reach. If a variable, a list or a running function leads to it, it’s safe — and so is everything it leads to.
  • If you want something gone, check who’s holding it. The usual culprit is a list you forgot it was in: every monster ever spawned, still sitting in horde long after it was beaten.

From Python: The same model as CPython: counted, with a collector for cycles. There’s no del and no gc module to poke.

From JavaScript: No need to wait for a collector for most things — a value is freed when its last holder goes. Rings take a moment longer.

From Lua: Like your collector, but most memory comes back straight away. No collectgarbage to call.

From BASIC: No ERASE and no REDIM to shrink things. Give the variable something else — NULL, or [] — and the old value is gone.

Crash-tan’s Tip: At the end of a level, give the big lists fresh empty ones: horde = []. Everything that was only in the old list goes with it, in one line.

Other ways to write things

Sometimes a statement wants a shape that a keyword and its modifiers can’t give it. REWRITE tells me another way a line may be written, and what to read it as.

REWRITE SHOW {first}; {second} => PRINT {first}, {second}

SHOW "lives"; 3
SHOW 1 + 1; "is two"
lives 3
2 is two

Before the => is the shape: the words and marks that have to be there, and names in braces standing for whatever is written in their place. After it is what the line becomes. I swap one for the other before I look at anything else, on the same line, so an error still points at the line you wrote.

Reading old code

This is how a script written for an older BASIC can run as it is. A handful of rules, usually kept in a file of their own and imported:

REWRITE IF {test} THEN => IF {test}
REWRITE ELSEIF {test} THEN => ELSE IF {test}
REWRITE SUB {name}({params}) => FUNCTION {name}({params})
REWRITE END SUB => END FUNCTION
REWRITE CALL {name}({values}) => {name}({values})
REWRITE PRINT {first}; {rest} => PRINT {first}, {rest}

SUB report(score)
    IF score >= 90 THEN
        PRINT "score"; score; "is great"
    ELSEIF score >= 50 THEN
        PRINT "score"; score; "is fine"
    ELSE
        PRINT "score"; score; "needs work"
    END IF
END SUB

CALL report(95)
CALL report(20)
score 95 is great
score 20 needs work

Every line under those rules is the old way of writing it, and I read it without complaint.

A few things to know:

  • The shape is the whole statement. A rule can’t change a piece in the middle of a line.
  • The first rule that fits wins, so put the longer shape first.
  • What a rule makes is tried against the rules again. That’s why one rule for ; handles a PRINT with any number of them.

From BASIC: This is for you! If you’re bringing old code over, import a file of these rules and port it a piece at a time.

From Python, JavaScript and Lua: It’s a very small macro: one line in, one line out, by shape. No syntax trees, nothing to learn.

Crash-tan’s Tip: Reach for BIND first. A rule is for the shapes a keyword can’t have, like an option word after the values or a mark I don’t otherwise know.

Where you came from

That’s the language! Here’s the lot on one page, next to what you’d have written before. Keep it open while your fingers catch up.

You wantCrash BASICBASICPythonJavaScriptLua
A comment// note' note# note// note-- note
NothingNULL—Nonenullnil
Equal, not equal= <>= <>== !==== !==== ~=
And, or, notAND OR NOTthe sameand or not&& || !and or not
Whole division7 \ 27 \ 27 // 2Math.trunc(7 / 2)7 // 2
Remainder7 MOD 27 MOD 27 % 27 % 27 % 2
Power2 ^ 102 ^ 102 ** 102 ** 102 ^ 10
Join text"a" + b"a" + b$"a" + str(b)"a" + b"a" .. b
Many-line text`...`—"""..."""`...`[[...]]
A list[1, 2]DIM a(1)[1, 2][1, 2]{1, 2}
First itemlist(0)a(0)list[0]list[0]list[1]
A piecelist(1 ... 3)—list[1:4]list.slice(1, 4){table.unpack(list, 2, 4)}
A piece of texts(0 ... 2)LEFT$(s, 3)s[0:3]s.slice(0, 3)s:sub(1, 3)
Matrix producta @* b—a @ b (NumPy)a librarya library
How manyLEN(list)UBOUND(a)len(list)list.length#list
Add to the endPUSH list, x—list.append(x)list.push(x)table.insert(list, x)
A map{hp: 1}—{"hp": 1}{hp: 1}{hp = 1}
Read a keym.hp m(key)—m["hp"]m.hp m[key]m.hp m[key]
Else-ifELSE IFELSEIFelifelse ifelseif
Choose by valueSELECT CASESELECT CASEmatchswitch—
CountFOR i = 1 ... 5FOR i = 1 TO 5for i in range(1, 6)for (let i = 1; i <= 5; i++)for i = 1, 5 do
Each itemFOR x = listFOR EACH x IN afor x in listfor (const x of list)for _, x in ipairs(list) do
Leave a loopBREAKEXIT FORbreakbreakbreak
A functionFUNCTION f(a)SUB / FUNCTIONdef f(a):function f(a) {}function f(a) end
Its own variableLOCAL x = 1LOCAL x(the default)let x = 1local x = 1
The rest of the argumentsrest...—*rest...rest...
A small functionn => n * 2—lambda n: n * 2n => n * 2function(n) return n * 2 end
A recordTYPETYPEclassclassa table
Itself, in a methodSELF—selfthisself
A set of named numbersENUMENUMEnuma frozen objecta table
A constantCONST x = 1CONST x = 1X = 1const x = 1local x <const> = 1
Another fileIMPORT "lib"INCLUDEimport libimport "./lib.js"require("lib")
Free something(let go of it)ERASE a(let go of it)(let go of it)(let go of it)
Start in the backgroundASYNC f()—a threada promisea coroutine
Wait for itRESOLVE(future)—.result()awaitcoroutine.resume

Things only I have

  • ## and _ — pass a value along a chain of steps.
  • BIND — turn a function into a keyword.
  • !!, the WOMT operator — Wait! One more thing! Named options on the end of a statement.
  • BLOCKBIND, SCOPEBIND, HANDLERBIND — keywords that own a block.
  • Errors as values — declared with the message a person will read.
  • REWRITE — let a line be written another way.
  • Coordinates and vectors — (x, y) and (a) -> (b), built in.
  • Matrices — turn @* ship: a list of rows has @*, @+ and @-.

Where next

Everything here was just me, on my own. Inside a game engine I’m given a great many more words — for drawing, sound, input and the rest — every one of them made the way you’ve just learned. And now you know how to add your own.

Go and make something! I’ll be here when it goes sideways.


Crash-tan, her name and her likeness are © Crash Continuum LLC, all rights reserved. She is not covered by the license Crash BASIC is offered under.

Your first host

Everything so far was about writing Crash BASIC. This part is about the other side: putting me inside a program of your own, so that your users can write it.

The program I live in is called the host. I’m a Rust library, and a small one: I read scripts, check them, and run them. Everything a script can actually do — draw, make a noise, move a character, save a file — is yours to give me. Out of the box I don’t even have PRINT.

That’s the whole idea. A game engine gives me sprites and sound. A level editor gives me tiles. A robot gives me motors. The language stays the same and the words change.

Getting me

I’m a Rust crate called crashbasic. Point Cargo at wherever you keep me:

[dependencies]
crashbasic = { path = "../crashbasic_interpreter" }

I need Rust 1.88 or newer, and I bring very little with me: a parser library, and serde for writing my reference as JSON. I build for desktop, phones and the web.

The smallest host

    use crashbasic::{Engine, Source, Value};

    let mut engine = Engine::new();

    // A function is its declaration, written as a script would write it,
    // and a closure for its body.
    engine.function("/// Writes its values out.\nFUNCTION write_line(values...)", move |call| {
        if let Value::Array(values) = &call.args[0] {
            let shown: Vec<String> = values.read().unwrap().iter().map(Value::to_string).collect();
            lines.lock().unwrap().push(shown.join(" "));
        }
        Value::Null
    })?;
    // With `typed`, the closure takes Rust types and I check the arguments.
    engine.typed("/// Twice a number.\nFUNCTION double(n AS INTEGER) AS INTEGER", |n: i64| n * 2)?;
    // A keyword is a word bound to a function.
    engine.bind("BIND PRINT write_line")?;

    let script = engine.load(&Source {
        file: "hello.crash".to_string(),
        text: "PRINT \"twice 21 is\", double(21)".to_string(),
    })?;
    script.run_all(10_000)?;

Four things happened there, and they’re the four things every host does.

  1. Made an engine. An Engine holds everything you give me. You set it up once.
  2. Gave me functions. Each is a declaration, written exactly the way a script would write it, and a Rust closure to run when it’s called.
  3. Bound a keyword. BIND PRINT write_line is the same line a script would write. It gives the function the word scripts will use for it.
  4. Loaded a script and ran it. Loading is where I check everything. If load gives you a script, every name and keyword in it is real.

The script itself is one line:

PRINT "twice 21 is", double(21)

What I do, and what you do

I doYou do
Read scripts and check them before they runDecide what words scripts get
Run them, in slices, on as many threads as you give meDecide when each thread runs, and for how long
Keep values, and free them when nothing holds themDo everything that touches the world outside
Say what went wrong, in words a person can useShow that to the person
Document every function and keyword you declareWrite one line of description above each

The rest of this part takes those one at a time.

Crash-tan’s Tip: The test harness that comes with me, in harness/, is a complete small host: PRINT, FOR, timers, a window to draw in, a debugger console. When you want to see how something here looks in a real program, it’s all there in one file.

Giving me functions

A function you give me is the same kind of thing as one a script writes. Scripts can call it, pass it around, bind a keyword to it, and read its documentation. The only difference is where its body lives.

The declaration

You write the declaration as text, in my own syntax:

/// Keeps a number between two others.
/// !example: PRINT clamp(250)
FUNCTION clamp(n, low = 0, high = 100)

That one string gives me the name scripts call it by, its parameters and their defaults, and its documentation. The Rust closure’s own name never reaches a script.

Closures that take Rust types

typed is the easy way. Write the closure with ordinary Rust parameters and I’ll convert each argument, and stop the script with a proper message if one is the wrong kind.

    // Defaults are written in the declaration; the closure always gets a value.
    engine.typed("FUNCTION clamp(n, low = 0, high = 100)", |n: i64, low: i64, high: i64| {
        n.clamp(low, high)
    })?;
    // `Option` takes NULL, `Vec` takes an array or the rest of the arguments,
    // and a pair is a coordinate.
    engine.typed("FUNCTION shout(text, mark = NULL)", |text: String, mark: Option<String>| {
        format!("{}{}", text.to_uppercase(), mark.unwrap_or_else(|| "!".to_string()))
    })?;
    engine.typed("FUNCTION total(numbers...)", |numbers: Vec<f64>| numbers.iter().sum::<f64>())?;
    engine.typed("FUNCTION mirror(spot)", |(x, y): (i64, i64)| (-x, y))?;
In the closureTakes
i64A whole number
f64Any number
boolAnything, judged the way IF judges it
StringText
(i64, i64) or (f64, f64)A coordinate
Option<T>NULL, or a T
Vec<T>A list, or a rest... parameter
ValueAnything at all

It can return any of those, () for NULL, or a Value. Up to eight parameters.

If a script calls clamp("lots"), it stops on that line with: “clamp needs a whole number for n, and it was given a string!” You wrote none of that.

Closures that take the call

function hands you the call as it is: every argument as a Value, with the defaults already filled in.

    // `function` hands over the call itself: every argument as a `Value`,
    // the parameters' names, and the file and line the call was written on.
    engine.function("FUNCTION describe(value)", |call| {
        let said = format!("{} on line {} is {}", call.params[0], call.line, call.args[0].type_name());
        Value::string(&said)
    })?;

A Call has:

FieldIs
argsOne value for each parameter. A rest... parameter is one array.
paramsThe parameters’ names
functionThe function’s name, for messages
file, lineWhere the call was written
filesThe files you gave me (see Files)

Closures have to be Send + Sync: scripts may call them from several threads at once. Share state the usual way, with an Arc and a lock or an atomic.

When it doesn’t work out

Remember the two kinds of “wrong” from When things go wrong? A host function chooses between them the same way a script’s does.

    use std::sync::Arc;

    use crashbasic::{Error, Reply, Value};

    // A result the script is expected to look at: an error handed back as a
    // value. The doc comment declares it, with the words a person will read.
    let find = "/// Finds a saved game.
/// !save-missing: There's no save called `{name}`!
/// !tip: Saves are made with `SAVE`.
FUNCTION find_save(name)";
    engine.function(find, |call| match call.args[0].to_string().as_str() {
        "slot1" => Value::string("level 3"),
        name => {
            let values = vec![("name".to_string(), name.to_string())];
            let (code, file) = ("save-missing".to_string(), call.file.to_string());
            Value::Error(Arc::new(Error { code, file, line: call.line, values }))
        }
    })?;

    // A mistake in the script: `Reply::fail` stops it on the calling line.
    let volume = "/// Sets how loud things are.
/// !volume-out-of-range: A volume of `{given}` is no good!
/// !tip: Give a number from 0 to 10.
FUNCTION set_volume(level)";
    engine.function(volume, |call| match call.args[0] {
        Value::Integer(level) if (0..=10).contains(&level) => Reply::Value(Value::Null),
        ref other => Reply::fail("volume-out-of-range", &[("given", &other.to_string())]),
    })?;
  • A result — “no such save” — goes back as an error value. The script can check it with ISERROR and carry on. If it doesn’t check and uses the value anyway, it stops there.
  • A mistake — a volume of 11 — stops the script at once, with Reply::fail.

Either way the error is declared in the doc comment: its name, what I should say, and a tip. That’s where the words a person reads come from, and it’s why they show up in the generated reference.

When a script does stop, run gives you the error, and the script turns it into those words:

    // stopped: the error `run` or `run_all` gave back.
    let said = script.report(&stopped);
main.crash:5: A volume of `11` is no good!
    Crash-tan's Tip: Give a number from 0 to 10.

Things read like variables

Some things a script only wants to read: the time, the screen’s width, how many lives are left. Declare those with value and they’re read with no parentheses, and worked out fresh each time.

    use std::sync::atomic::{AtomicI64, Ordering};

    let lives = Arc::new(AtomicI64::new(3));

    // A value is read with no parentheses, and worked out each time.
    let read = lives.clone();
    engine.value("/// How many lives are left.\nFUNCTION lives() AS INTEGER", move |_| {
        Value::Integer(read.load(Ordering::Relaxed))
    })?;
    engine.value("FUNCTION screen.width() AS INTEGER", |_| Value::Integer(320))?;

    // An accessor is a function scripts can also assign through. The
    // second closure is given the same arguments, and the value assigned.
    let settings: Arc<Mutex<BTreeMap<String, Value>>> = Arc::default();
    let (get, set) = (settings.clone(), settings.clone());
    engine.accessor(
        "/// A setting, kept between runs.\nFUNCTION setting(name)",
        move |call| get.lock().unwrap().get(&call.args[0].to_string()).cloned().unwrap_or(Value::Null),
        move |call, value| {
            set.lock().unwrap().insert(call.args[0].to_string(), value.clone());
            Value::Null
        },
    )?;
PRINT lives, lives - 1, screen.width > 300
setting("name") = "Ada"
PRINT setting("name")

A value can’t be assigned to, but a script can take its name. If a script makes a variable called lives, the variable wins everywhere in that script and your lives is out of its reach. The same goes for your functions. A script’s author shouldn’t have to know every name you’ve declared before choosing theirs.

Something scripts both read and assign is an accessor: a function with a second closure that takes the value being assigned.

Crash-tan’s Tip: Write the doc comment. One line above the declaration and the function explains itself in the editor, in the reference, and in my error messages. It’s the cheapest documentation you will ever write.

Giving me keywords

A keyword is a word bound to a function. You bind them with the very lines a script would write, handed to engine.bind. So everything in Your own keywords and Keywords with blocks is yours to use from the host too, and what you bind there is how your program’s own vocabulary gets made.

A statement, with options

    // The function behind the keyword, with a parameter for each option.
    let line = "FUNCTION line(span, color = \"white\", thick = 1, dashed = FALSE)";
    engine.typed(line, move |span: Value, color: String, thick: i64, dashed: bool| {
        let dashes = if dashed { ", dashed" } else { "" };
        lines.lock().unwrap().push(format!("{span} in {color}, {thick} thick{dashes}"));
    })?;
    // Each modifier is a function that returns the parameters it fills.
    // In a namespace of their own, the words stay free for variables.
    engine.function("FUNCTION Pen.thick(size)", |call| {
        Value::map(BTreeMap::from([("thick".into(), call.args[0].clone())]))
    })?;
    engine.function("FUNCTION Pen.dashed()", |_| {
        Value::map(BTreeMap::from([("dashed".into(), Value::Integer(1))]))
    })?;
    engine.bind("BIND LINE line ALLOW(Pen)")?;
LINE (0, 0) -> (9, 9), "red" !! thick(3) dashed
LINE (1, 1) -> (2, 2)

The function has a parameter for every option, each with a default. A modifier is a function that returns a map naming the parameters it fills. ALLOW(Pen) lets the keyword take every function in the Pen namespace as a modifier, by its short name.

Putting modifiers in a namespace matters more than it looks. A real engine has hundreds of option words: speed, size, name, repeat. If each were a plain function, no script could ever have a variable called speed. In a namespace, the short word only means something after !!.

A function takes modifiers the same way, without a keyword: put ALLOW on its declaration, FUNCTION open(path, append = FALSE) ALLOW(Files), and a script writes file = OPEN("scores.txt") !! append. Use that for anything that gives a value back. A parameter named in ALLOW with ... after it, ALLOW(values...), takes every other word after !! as a map, for names you can only know when the call runs.

Keywords that own a block

There are four kinds, and they differ in what becomes of the block.

The block runs once for each value

    // BLOCKBIND: the block runs once for each value the function gives.
    // An iterator gives them one at a time, as the block asks.
    engine.typed("FUNCTION countdown(from)", |from: i64| {
        Value::iterator((1..=from).rev().map(Value::Integer))
    })?;
    engine.bind("BLOCKBIND COUNTDOWN countdown")?;
COUNTDOWN n = 3
    PRINT n
END COUNTDOWN

The function gives back what to step through: a list, a range, a map, or an iterator. Value::iterator wraps any Rust iterator, and I ask it for one value per pass, so it can be as long as it likes, or not known in advance. FOR itself is nothing more than this.

The block runs once, with something around it

    // SCOPEBIND: the block runs once. The first function makes the block's
    // value; the one after END is handed it when the block is over.
    engine.typed("FUNCTION menu_open(title)", |title: String| {
        Value::map(BTreeMap::from([
            ("title".into(), Value::string(&title)),
            ("items".into(), Value::array(Vec::new())),
        ]))
    })?;
    // Anything called inside the block can ask for that value.
    engine.function("FUNCTION menu_item(label)", |call| -> Result<Value, Error> {
        if let Value::Map(menu) = call.scope("MENU")?
            && let Some(Value::Array(items)) = menu.read().unwrap().get("items")
        {
            items.write().unwrap().push_back(call.args[0].clone());
        }
        Ok(Value::Null)
    })?;
    let shown = printed.clone();
    engine.function("FUNCTION menu_close(menu)", move |call| {
        shown.lock().unwrap().push(format!("menu: {}", call.args[0]));
        Value::Null
    })?;
    engine.bind("SCOPEBIND MENU menu_open END menu_close\nBIND ITEM menu_item")?;
MENU "Start"
    ITEM "play"
    ITEM "quit"
END MENU

The first function runs before the block and returns the block’s value. The function after END is handed that value when the block is over. In between, any function called inside the block — however deep — can ask for it with call.scope("MENU"). Outside a MENU block that’s an error, and the ? stops the script with a message saying ITEM belongs inside one.

This is how you give scripts a little language for describing things: a menu, a level, a particle effect, a song.

The block is kept for later

    // HANDLERBIND: the block is not run. It arrives as the last argument, a
    // function to keep and call later.
    let handlers: Arc<Mutex<BTreeMap<String, Value>>> = Arc::default();
    let kept = handlers.clone();
    engine.function("FUNCTION on_event(name, handler)", move |call| {
        kept.lock().unwrap().insert(call.args[0].to_string(), call.args[1].clone());
        Value::Null
    })?;
    engine.bind("HANDLERBIND ON on_event GIVES(amount)")?;
bank = 100
ON "coin"
    bank = bank + amount
    PRINT "banked", bank
    RETURN bank
END ON

The block is handed over. It arrives as the function’s last argument: a function value, which you keep. GIVES(amount) names what the block will be told each time it’s called.

When the moment comes, call it:

    // Later, when a coin is picked up: the block is called with the value
    // GIVES named, on a thread of its own.
    let handler = handlers.lock().unwrap()["coin"].clone();
    let mut thread = script.call(&handler, vec![Value::Integer(5)])?;
    while thread.run(10_000)? != Status::Finished {}
    let returned = thread.result();

script.call gives you a thread, which you run like any other. The block keeps a copy of the locals that were around it when it was written, and can read and change the script’s shared variables, as bank is here.

The block is text

    // TEXTBIND: the block is not read. Its lines arrive as the last
    // argument, as one string.
    let statements: Arc<Mutex<BTreeMap<String, String>>> = Arc::default();
    let kept = statements.clone();
    engine.function("FUNCTION define_sql(name, statement)", move |call| {
        kept.lock().unwrap().insert(call.args[0].to_string(), call.args[1].to_string());
        Value::Null
    })?;
    engine.bind("TEXTBIND SQL define_sql AS \"sql\"")?;
SQL "top"
    SELECT name, points FROM scores
    WHERE points >= @least
END SQL

I don’t read the lines inside. They arrive as the function’s last argument, one string, moved left as far as the least indented line. Use this when your program already has a language of its own for something: queries, shaders, level maps, dialogue.

Two things make it pleasant to work with:

  • call.line is the keyword’s line, and the text starts on the one after. When you find a mistake on line 2 of the text, you can say which line of the script that is.
  • AS "sql" is passed on to editors. document.texts() lists each block with its place in the file and its kind, so an editor that knows SQL can color it. See the editor chapter.

Crash-tan’s Tip: Before adding a keyword, try the plain function. A script can already write shake 0.5 for a function called shake. A keyword earns its place when it has two words, takes modifiers, or owns a block.

Running a script

I never take over your program. A script runs when you say so, for as long as you say, and then hands control back.

The short way

script.run_all(10_000)?;

run_all runs the main program and every thread it starts, in turns, on the calling thread, until all of them are done. It’s the right thing for a command-line tool or a test. A program with a screen to draw wants more say than that.

A frame at a time

    let mut main = Some(script.start());
    let mut others: Vec<Thread> = Vec::new();
    while main.is_some() || !others.is_empty() {
        // One frame: every thread gets a slice of 500 loop passes and calls.
        if let Some(thread) = &mut main {
            match thread.run(500)? {
                Status::Finished => main = None,
                // Out of budget, waiting on a host function, or stopped by
                // the debugger: all mean "come back to me next frame".
                Status::Yielded | Status::Waiting | Status::Stopped(_) => {}
            }
        }
        // The others the same way. One that has finished is dropped.
        let mut at = 0;
        while at < others.len() {
            match others[at].run(500)? {
                Status::Finished => drop(others.swap_remove(at)),
                _ => at += 1,
            }
        }
        // Threads that ASYNC calls started during this frame.
        others.extend(script.take_spawned());

        // ...draw the frame, read the controller, play the sound...
    }

script.start() gives you the main program as a thread. A thread of mine is a stack and a place in the code, far lighter than one of the operating system’s, and it does nothing until you call run.

run(budget) runs it until one of these happens:

It returnsBecauseYou
Status::FinishedThe thread reached its endDrop it
Status::YieldedThe budget ran outRun it again next frame
Status::WaitingA host function is waiting for somethingRun it again later; it asks again each time
Status::Stopped(…)The debugger stopped itShow the debugger, then run it again
an errorThe script made a mistakeShow script.report(&error)

The budget counts loop passes and function calls, which are the only ways a script can run for long. A script with an endless loop costs you one budget per frame and nothing more. It can’t hang your program.

Waiting without blocking

A host function must never sit and wait: it would hold up everything else. When the answer isn’t ready, it returns a poll instead.

    // A function that has to wait does not block. It gives a poll, and the
    // thread that called it steps aside until the poll has an answer.
    let door = door_open.clone();
    engine.function("/// Waits until the door is open.\nFUNCTION wait_for_door()", move |_| {
        let door = door.clone();
        Reply::wait(move || door.load(Ordering::Relaxed).then(|| Value::string("open")))
    })?;

The thread that called it steps aside (Status::Waiting), and each time you run it again I ask the poll. None means “not yet”. Some(value) becomes what the function returned, and the script carries on from there.

To the script, it just looks like a function that took a while:

PRINT "the door is", wait_for_door()

This is how you write DELAY, wait for a key, wait for a download, or wait for an animation to finish.

Sometimes blocking is what you have: a call into another part of your program that waits for the next frame, say. Then wrap the part that blocks in call.outside, so the rest of the script isn’t kept waiting on it:

engine.function("FUNCTION next_frame()", |call| {
    call.outside(|| frames.wait_for_next());
    Value::Null
})?;

Threads

Every ASYNC call in a script makes another thread. They pile up until you ask for them with script.take_spawned(), and then they’re yours to run however you like:

  • In turns, on one thread, as the loop above does. Simple, and all a great many programs need.
  • On a pool of real threads. A Thread can be sent to another core and run there. Scripts that share variables are safe to run side by side: every read and write of a shared value is a single step, and ATOMIC variables make a read-change-write one step too.

Nothing about a script says which you chose. The same ASYNC and RESOLVE work either way.

A host function can end the thread that called it by returning Reply::end(): no error, it just stops there. To stop one thread from outside, call thread.cancel(). To stop every thread of a script, as when the main program ends and the rest shouldn’t outlive it, call script.cancel_all(). run_main is run_all with that built in.

In a browser

Built for the web in the ordinary way, I run on the page’s single thread and script threads take turns. This book works that way.

For real parallel threads in a browser, build with WebAssembly threads switched on and run script threads on Web Workers that share memory. I need nothing special for it — I start no threads and never sleep — but keep script threads off the page’s main thread: a script thread may wait briefly for a value another is using, and a browser doesn’t let its main thread wait. The webtest/ folder is a working example that runs four threads on four workers.

Cleaning up

You don’t free anything, and neither do scripts: see Who cleans up?. Values are freed the moment nothing holds them.

The one thing worth knowing as a host: values that hold each other are found and freed in an occasional pass, which pauses the script’s threads for about a millisecond per ten thousand live lists, maps and objects. By default I choose the moment. If you’d rather choose it — between frames, on a loading screen — say so:

script.collect_by_hand(true);
// ...then, when it suits you:
if script.collect_due() {
    script.collect();
}

Anything you are holding is never freed by this, nor anything it leads to. You don’t have to tell me what you’re holding.

Sharing what we know

Scripts and the host live in the same memory. Passing something between us is direct: for most things, we both end up holding the very same value.

Values

Everything a script can hold is a Value.

A script’sIs a Value::You get at it with
Whole numberInteger(i64)the number
Number with a fractionFloat(f64)the number
TextStrto_string()
NULLNull
CoordinateCoordinate(x, y) and friendscomponent(n)
ListArrayread() / write() on what it holds
MapMapread() / write() on what it holds
An object of a TYPEObjectget("field") / set("field", value)
Function or blockFunction, Closurescript.call(&value, args)
ErrorErrorits code and values

Make them with Value::Integer(5), Value::string("hi"), Value::array(vec![…]) and Value::map(…). value.to_string() gives what PRINT would show, and value.type_name() gives words for a message: “an array”, “a string”.

A script’s variables

    // A variable is read and written by name. In a namespace it has its
    // full name: `army.recruits`.
    let score = script.global("score");
    assert!(matches!(score, Some(Value::Integer(7))));
    script.set_global("score", Value::Integer(8));

    // An object's fields are read and written by name too.
    if let Some(Value::Object(hero)) = script.global("ada") {
        let hp = hero.get("hp");
        assert!(matches!(hp, Some(Value::Integer(10))));
        hero.set("hp", Value::Integer(99));
    }

One thing, two holders

This is the important one. Numbers, text and coordinates are copied when they’re passed. Lists, maps and objects are shared. If a script hands you a list and you keep it, you and the script are holding the same list.

    // An array, a map and an object are shared, not copied. Keep the one a
    // script hands over and both sides are looking at the same thing.
    let tracked: Arc<Mutex<Vec<Value>>> = Arc::default();
    let keep = tracked.clone();
    let track = "/// Has the engine move this position every frame.\nFUNCTION track(position)";
    engine.function(track, move |call| {
        keep.lock().unwrap().push(call.args[0].clone());
        Value::Null
    })?;
spot = [0, 0]
track(spot)

From then on, whatever you write into it, the script sees:

    // Each frame, the engine writes where the thing has got to. The script
    // sees it the next time it reads `spot`.
    for position in tracked.lock().unwrap().iter() {
        if let Value::Array(numbers) = position {
            let mut numbers = numbers.write().unwrap();
            numbers[0] = Value::Integer(120);
            numbers[1] = Value::Integer(64);
        }
    }

No message was sent and nothing was copied back. The next time the script reads spot(0), it’s 120.

This is how an engine keeps a script’s view of something up to date without the script asking: a position that physics moves, a list of who’s in a room, a table of high scores. Give the script the list (or let it give you one), keep hold of it, and write into it.

Each read and write is a single safe step, even with script threads running on other cores. Take the lock, do one thing, let it go: don’t hold a write() across a call back into a script.

Calling into a script

    // Any function of the script can be called from outside, by name.
    let report = script.function("report").expect("the script declares it");
    let mut thread = script.call(&report, Vec::new())?;
    while thread.run(10_000)? != Status::Finished {}

script.function finds a function by its full name: update, Army.recruit, Unit::Hit. What you get stays good even if the function is replaced later by live coding: the next call runs the new one. This is how an engine calls a script’s update every frame.

Between two machines

Two copies of me that loaded the same scripts can send values to each other. script.to_wire(&value) gives bytes and script.from_wire(&bytes) turns them back. Carrying the bytes is your business.

Nearly everything crosses, including functions, blocks with the locals they kept, and ASYNC calls that haven’t started — which is how you ask another machine to run something. What arrives is always a copy. Compare script.fingerprint() on both sides when they connect: it only matches if both loaded the same code.

Files

I never open a file myself, even for IMPORT. Every file a script touches comes through something you give me, so scripts can only reach what you decide they can.

Where scripts come from

/// Scripts kept wherever the host keeps things: here, inside the program.
struct Bundled;

const MATHS: &str = "NAMESPACE Maths
    FUNCTION twice(n)
        RETURN n * 2
    END FUNCTION
END NAMESPACE";

impl Files for Bundled {
    fn read_text(&self, path: &str) -> Result<String, String> {
        match path {
            "main.crash" => Ok("IMPORT \"lib/maths\"\nPRINT Maths.twice(21)".to_string()),
            "lib/maths.crash" => Ok(MATHS.to_string()),
            _ => Err("there is no such file".to_string()),
        }
    }
}
    engine.files(Bundled);
    let script = engine.load_file("main.crash")?;

Files is a trait. Only read_text is required. The rest — read_binary, write_text, write_binary, exists, delete, list — refuse unless you provide them, so nothing ever reports a save that didn’t happen.

Two kinds come with me:

  • DiskFiles::new("games/asteroids") is one folder on disk.
  • MemoryFiles::new().with("main.crash", text) keeps everything in memory, which is what tests and this book use.

Whichever you use:

  • IMPORT "lib/maths" reads lib/maths.crash through it.
  • Your own functions reach it as call.files, so a LOAD you write sees exactly the files IMPORT does.
  • An IMPORT can’t climb out. Its path uses /, is relative to the file doing the importing, and is refused if it would leave the root you chose, with .. or an absolute path. DiskFiles refuses the same for every path it’s given.

Scripts that aren’t in files

A script doesn’t have to come from a file at all. engine.load takes the text directly, with a name to report errors against. Its imports still go through your Files.

let script = engine.load(&Source { file: "console".to_string(), text: typed_by_the_user })?;

Crash-tan’s Tip: A file ending in .bas is the older Crash BASIC, and I’ll say so if you hand me one. Mine end in .crash.

Tools for whoever writes the scripts

Embedding a language is only half the job. Somebody has to write in it, and they’ll want to know what’s wrong, what’s available, and what their script is doing. I can answer all of that, so your program can.

What’s wrong with this script?

load stops at the first mistake. check finds all of them.

    // Everything wrong with a script, each line once, without running it.
    let text = "total = 0\nPRNT total\nFOR i = 1 ... 3\n    total = total + nobody\nNEXT";
    let script = Source { file: "main.crash".to_string(), text: text.to_string() };
    for mistake in engine.check(&script) {
        // main.crash:2: Hmm, I don't know the word `PRNT`! ...
        // main.crash:4: Hmm, I don't know the name `nobody`! ...
        shown.push(engine.declarations().messages().report(&mistake));
    }

Each mistake is reported once. A loop whose first line is wrong still counts as a loop, so its body and its END are judged as usual.

What an editor asks

Give me the text as it is right now — half typed, broken, whatever — and I’ll answer an editor’s questions about it. Nothing is run.

    // What an editor asks, of text that may be half typed.
    let text = "/// How far to jump.
FUNCTION jump(height = 3)
    RETURN height * 2
END FUNCTION
PRINT jump(1)
PRINT ju";
    let document = engine.open("main.crash", text);

    // What could come next, where the cursor is: at the end, after `ju`.
    let offered = document.completions(text.chars().count());
    assert!(offered.items.iter().any(|item| item.label == "jump"));

    // What the word under the cursor is, to show beside it.
    let about = document.hover(text.find("jump(1)").unwrap()).expect("`jump` is known");
    assert_eq!(about.description, "How far to jump.");

    // Colours for every word, and what is wrong, if anything.
    let _spans = document.highlight();
    let _problems = document.diagnostics();

There’s also signature, for showing a function’s parameters while its arguments are being typed, and definition, for jumping to where something was declared. Completions know where they are: after END they offer the blocks that are open, after !! the modifiers that keyword allows, after a dot the fields of that type.

Everything you declared is in there, with the doc comment you wrote.

A debugger

Every script has one, and it costs nothing until it’s used.

    let debugger = script.debugger();
    // Stop on line 3, but only on the pass where `height` is 2.
    let when = Breakpoint { condition: Some("height = 2".to_string()), ..Breakpoint::default() };
    debugger.set_breakpoint_with("main.crash", 3, when);

    let mut thread = script.start();
    let stop = loop {
        match thread.run(1_000)? {
            Status::Stopped(stop) => break stop,
            Status::Finished => unreachable!("the breakpoint is reached first"),
            _ => {}
        }
    };
    assert_eq!((stop.file.as_str(), stop.line), ("main.crash", 3));

    // Where it is, and what it can see.
    let calls: Vec<String> = thread.frames().iter().map(|at| format!("{}:{}", at.name, at.line)).collect();
    assert_eq!(calls, ["jump:3", ":6"]);
    let landed = thread.inspect(0, "landed").expect("a local of `jump`");
    assert!(matches!(landed, Value::Integer(4)));

    // Work something out there, change something, and take one step.
    let worked_out = thread.evaluate(0, "landed + height")?;
    assert!(matches!(worked_out, Value::Integer(6)));
    thread.assign(0, "landed", Value::Integer(1000));
    thread.step(Step::Out);

Nothing blocks. A thread that reaches a breakpoint returns Status::Stopped from run, and you decide what happens next. While it’s stopped you can read every call and variable, work out any expression where it stands, change things, and step.

It can also:

  • Stop where an error is made (break_on_errors), while the function that made it is still there to look at.
  • Stop when something changes (watch("hero.hp")), and say what it was and what it is now.
  • Take notes instead of stopping: a breakpoint with a log expression.

A thread that halts on a mistake keeps everything too, so you can show where it was and what it could see without having asked beforehand.

Changing a script while it runs

    // Code added to a script that is already running. A function declared
    // again takes over from its next call.
    let patch = "FUNCTION jump(height)
    RETURN height * 100
END FUNCTION
PRINT jump(2)";
    let mut added = script.push("patch.crash", patch, 1)?;
    while added.run(1_000)? != Status::Finished {}

push adds code to a running script. New functions, types and keywords are there for every thread from then on. A function declared again takes over from its next call; calls already inside the old one finish it. Nothing restarts and no variable is lost.

Wire that to “save” in an editor and people can change a game while they play it.

A reference that writes itself

    // Everything I know: the language, then what the host declared.
    let reference = engine.document();
    // Only what this script and the files it imports declare.
    let for_this_script = script.document();
    // The same as JSON, for a site or an editor to lay out its own way.
    let as_json = engine.document_json("1.0", "My engine's scripting reference");

Every function and keyword you declare, with its doc comment, its parameters and defaults, its examples, and the errors it can give. The JSON form carries the same, for a website or an editor to lay out its own way.

Doc comments take a few tags worth knowing: !example:, !returns:, !param name:, !category:, !since:, !see:, and !status: deprecated, which makes me warn whenever a script uses it.

Other ways to write things

    // Another way a statement may be written, for every script I load.
    engine.rewrite("REWRITE SAY {words} TWICE => PRINT {words}, {words}")?;

A rule you give the engine applies to every script it loads. That’s the place for the shapes your statements need that a keyword can’t have. See Other ways to write things.

Crash-tan’s Tip: You don’t have to build all of this on day one. A host is useful with functions, keywords and a run loop. But check and report are a few lines each, and they’re the difference between someone enjoying your scripting and someone giving up on it.