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.