Hello!
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'orREM. 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 = 3can’t be a variable, because I couldn’t tell it from the statement. - Your own functions and types. If your program declares
FUNCTION score(), thenscore = 3is a mistake, and I’ll say so.
From BASIC: No
$or%on the end of a name.name$is justname, and a variable can hold a different kind of value later on.
From Python:
TRUE,FALSEandNULLare yourTrue,FalseandNone.TRUEreally is the number 1.
From JavaScript: There’s one “nothing”,
NULL. Noundefined.
From Lua:
NULLis yournil.
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$andRIGHT$are a range in parentheses now:LEFT$(name, 5)isname(0 ... 4).
From Python and JavaScript:
name[0:5]andname.slice(0, 5)arename(0 ... 4): the end is included.
From Lua:
name:sub(1, 5)isname(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 ... 5includes 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:
\,MODand^.
From Python:
\is your//,MODis your%, and^is your**.
From JavaScript:
^is a power here, and your XOR is~.MODis your%.
From Lua:
\is your//andMODis 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,ORandNOTwork on yes and no here. The bit operators are below.
From JavaScript:
ANDandORgive 1 or 0. They don’t hand back one of their two values, soname OR "nobody"won’t give you a default.
From Lua: The same goes for you:
x OR ygives 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
IFdoesn’t end its first line withTHEN, and it’sELSE 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 IFis yourelif. No colons.
From JavaScript: No parentheses needed around the condition, and no braces around the block.
From Lua:
ELSE IFis yourelseif, and there’s nothenon 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 thebreak. 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 iworks as written.STEPis the modifier!! step(2).
From Python:
FOR item = listis yourfor item in list, and the two-name form is yourenumerate.
From JavaScript: One
FORdoes the work offor,for...ofandfor...in.
From Lua: No
pairsoripairs: handFORthe 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 TRUEwith 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 INTEGERisscores = 10:INTEGER, and that’s 10 items, numbered 0 to 9.
From Python:
scores[1:4]isscores(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)isscores(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
LOCALfor the function’s own.
From Python: The other way round from you! No
globalneeded to change a shared variable — and you needLOCALto not share one.
From JavaScript:
LOCALis yourlet. 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 TYPEas you know it, plus defaults and functions of its own.
From Python: A
TYPEis a small class: fields with defaults, and methods.SELFisself, and you don’t list it as a parameter. There’s no inheritance.
From JavaScript:
SELFisthis, without the surprises. There are no prototypes and nonew.
From Lua: No metatables to set up.
thing.Hit(5)passesSELFfor you, so there’s no:to remember.
Crash-tan’s Tip: The
AS STRINGpart 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 ENUMisENUM, and the members are reached through its name:Mode.idle.
From Python: An
Enumwhose members are plain numbers.Mode.idleis just0.
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.areacame 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*argsand**kwargsat the call, in one operator: a list spreads in order and a map spreads by name.
From JavaScript:
**_is...valuefor 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 * 2is yourlambda n: n * 2, and the long form is adefyou 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
GOSUBtricks 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
TIMERandPIare made now, and how every statement you remember —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
SAYis bound, a line can’t beginsay = 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
ENDand the keyword’s own words close it. AnEND IFin 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 WHENison("coin", (amount) => { ... })with the punctuation taken away.
From Python: A
SCOPEBINDis awithblock you can build in three lines, and aHANDLERBINDis a decorator that takes a block. ATEXTBINDblock 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,WITHand 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 andRESOLVEisawait— 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
hordelong after it was beaten.
From Python: The same model as CPython: counted, with a collector for cycles. There’s no
deland nogcmodule 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
collectgarbageto call.
From BASIC: No
ERASEand noREDIMto 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 aPRINTwith 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
BINDfirst. 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 want | Crash BASIC | BASIC | Python | JavaScript | Lua |
|---|---|---|---|---|---|
| A comment | // note | ' note | # note | // note | -- note |
| Nothing | NULL | — | None | null | nil |
| Equal, not equal | = <> | = <> | == != | === !== | == ~= |
| And, or, not | AND OR NOT | the same | and or not | && || ! | and or not |
| Whole division | 7 \ 2 | 7 \ 2 | 7 // 2 | Math.trunc(7 / 2) | 7 // 2 |
| Remainder | 7 MOD 2 | 7 MOD 2 | 7 % 2 | 7 % 2 | 7 % 2 |
| Power | 2 ^ 10 | 2 ^ 10 | 2 ** 10 | 2 ** 10 | 2 ^ 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 item | list(0) | a(0) | list[0] | list[0] | list[1] |
| A piece | list(1 ... 3) | — | list[1:4] | list.slice(1, 4) | {table.unpack(list, 2, 4)} |
| A piece of text | s(0 ... 2) | LEFT$(s, 3) | s[0:3] | s.slice(0, 3) | s:sub(1, 3) |
| Matrix product | a @* b | — | a @ b (NumPy) | a library | a library |
| How many | LEN(list) | UBOUND(a) | len(list) | list.length | #list |
| Add to the end | PUSH list, x | — | list.append(x) | list.push(x) | table.insert(list, x) |
| A map | {hp: 1} | — | {"hp": 1} | {hp: 1} | {hp = 1} |
| Read a key | m.hp m(key) | — | m["hp"] | m.hp m[key] | m.hp m[key] |
| Else-if | ELSE IF | ELSEIF | elif | else if | elseif |
| Choose by value | SELECT CASE | SELECT CASE | match | switch | — |
| Count | FOR i = 1 ... 5 | FOR i = 1 TO 5 | for i in range(1, 6) | for (let i = 1; i <= 5; i++) | for i = 1, 5 do |
| Each item | FOR x = list | FOR EACH x IN a | for x in list | for (const x of list) | for _, x in ipairs(list) do |
| Leave a loop | BREAK | EXIT FOR | break | break | break |
| A function | FUNCTION f(a) | SUB / FUNCTION | def f(a): | function f(a) {} | function f(a) end |
| Its own variable | LOCAL x = 1 | LOCAL x | (the default) | let x = 1 | local x = 1 |
| The rest of the arguments | rest... | — | *rest | ...rest | ... |
| A small function | n => n * 2 | — | lambda n: n * 2 | n => n * 2 | function(n) return n * 2 end |
| A record | TYPE | TYPE | class | class | a table |
| Itself, in a method | SELF | — | self | this | self |
| A set of named numbers | ENUM | ENUM | Enum | a frozen object | a table |
| A constant | CONST x = 1 | CONST x = 1 | X = 1 | const x = 1 | local x <const> = 1 |
| Another file | IMPORT "lib" | INCLUDE | import lib | import "./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 background | ASYNC f() | — | a thread | a promise | a coroutine |
| Wait for it | RESOLVE(future) | — | .result() | await | coroutine.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.
- Made an engine. An
Engineholds everything you give me. You set it up once. - 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.
- Bound a keyword.
BIND PRINT write_lineis the same line a script would write. It gives the function the word scripts will use for it. - Loaded a script and ran it. Loading is where I check everything. If
loadgives 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 do | You do |
|---|---|
| Read scripts and check them before they run | Decide what words scripts get |
| Run them, in slices, on as many threads as you give me | Decide when each thread runs, and for how long |
| Keep values, and free them when nothing holds them | Do everything that touches the world outside |
| Say what went wrong, in words a person can use | Show that to the person |
| Document every function and keyword you declare | Write 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: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 closure | Takes |
|---|---|
i64 | A whole number |
f64 | Any number |
bool | Anything, judged the way IF judges it |
String | Text |
(i64, i64) or (f64, f64) | A coordinate |
Option<T> | NULL, or a T |
Vec<T> | A list, or a rest... parameter |
Value | Anything 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:
| Field | Is |
|---|---|
args | One value for each parameter. A rest... parameter is one array. |
params | The parameters’ names |
function | The function’s name, for messages |
file, line | Where the call was written |
files | The 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
ISERRORand 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.lineis 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.5for a function calledshake. 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 returns | Because | You |
|---|---|---|
Status::Finished | The thread reached its end | Drop it |
Status::Yielded | The budget ran out | Run it again next frame |
Status::Waiting | A host function is waiting for something | Run it again later; it asks again each time |
Status::Stopped(…) | The debugger stopped it | Show the debugger, then run it again |
| an error | The script made a mistake | Show 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
Threadcan 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, andATOMICvariables 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’s | Is a Value:: | You get at it with |
|---|---|---|
| Whole number | Integer(i64) | the number |
| Number with a fraction | Float(f64) | the number |
| Text | Str | to_string() |
NULL | Null | |
| Coordinate | Coordinate(x, y) and friends | component(n) |
| List | Array | read() / write() on what it holds |
| Map | Map | read() / write() on what it holds |
An object of a TYPE | Object | get("field") / set("field", value) |
| Function or block | Function, Closure | script.call(&value, args) |
| Error | Error | its 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"readslib/maths.crashthrough it.- Your own functions reach it as
call.files, so aLOADyou write sees exactly the filesIMPORTdoes. - An
IMPORTcan’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.DiskFilesrefuses 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
.basis 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
logexpression.
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
checkandreportare a few lines each, and they’re the difference between someone enjoying your scripting and someone giving up on it.