Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tools for whoever writes the scripts

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

What’s wrong with this script?

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

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

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

What an editor asks

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

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

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

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

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

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

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

A debugger

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

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

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

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

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

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

It can also:

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

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

Changing a script while it runs

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

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

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

A reference that writes itself

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

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

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

Other ways to write things

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

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

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