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.