Hello & Basics​

JaiScript is a C++-flavored embedded scripting language: snake_case, hot-reloadable, zero static state, and shipped with two interchangeable backends (a tree-walking interpreter and a bytecode VM) that are kept at full test parity. Every example in these docs was executed on both backends; unless a divergence is called out, the output shown is identical on each.

Running a script from C++​

C++
#include <jaiscript/core/engine.hpp>
#include <jaiscript/stdlib/stdlib.hpp>

auto eng = jai::engine::make();                 // shared_ptr<engine>
// eng->set_backend(jai::backend_type::interpreter);   // optional: tree-walking interpreter (before first execute; link jaiscript_interpreter)
jai::stdlib::register_all(eng);                 // formatted print, format, to_string, type_of, to_json, ...
jai::script_value r = eng->execute("2 + 2");
int v = r.as_int();                             // 4

Three things to know from the start:

  1. The last expression's value is the result of execute(). No return needed at top level.
  2. set_backend() must be called before the first execute() (forbidden after).
  3. The checker runs first, and it is strict. A typed mistake it can see (or an undeclared name) throws out of execute() before any statement runs. [[warn_mode]] turns a region's errors into warnings; see the Type Ladder. Chapter 13 collects the whole story.

Try it from a terminal​

You don't need a host program to follow this guide. The build produces a standalone runner, jaiscript: hand it a file and it runs it. Building it is one CMake target, from the repository root:

shell
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build --target jaiscript_runner

The executable lands in build/bin/jaiscript (the target is jaiscript_runner because the library owns the name jaiscript). With that on your path:

shell
jaiscript hello.jai            # run a file, then exit
jaiscript                      # interactive REPL (:help inside)
jaiscript --watch hello.jai    # re-run the file every time you save it

The REPL runs each entry as one execute() into a persistent engine, and an entry's last statement needs no ;, whatever its kind, just as the last expression of an execute() needs none: int hp = 100, hp += 1 and import "rules.jai" each run as typed. Statements inside braces keep the language's rule. An expression prints its value; a declaration prints nothing.

Three flags matter for the rest of these docs:

FlagWhat it does
--backend=vm|interpWhich backend executes the script. The runner defaults to vm; interp is the tree-walking interpreter. Both must produce the same answer. Running a script twice, once per backend, is how these pages are checked.
--warningsLists the static checker's warnings before the script runs. Errors (typed mistakes, undeclared names) always fail the load and print.
--budget=NSeconds of wall clock per execute. An overrun ends the run with a terminal error the script cannot catch (the host sees an exception; see Control Flow). Default 0: unlimited.

The runner's defaults are not the engine's. Both start on the VM, but an embedded jai::engine enforces a 5.0 second execution budget per execute() and the runner has no budget at all, because an infinite loop in a script you are editing is your own business. Static checking is strict in both.

One convention to know before you compare output. When a block on these pages ends in == result: …, that line is the value of the script's last expression: what execute() hands back to a host. The runner does not echo it after a file run (an int result becomes the exit code; the REPL echoes every non-null result), so wrap the last expression in print(...) if you want to see it in a terminal. Everything above that line is print output and matches character for character. Two more shapes you will meet: a block that ends in == error: … shows the message the host would receive from an uncaught error (the runner prints it to stderr and exits 1), and a block that starts jaiscript: static check failed: is the runner's own listing when the checker stops a load before anything runs.

Values and literals​

jaiscript
auto i = 42;            // integer (64-bit)
auto f = 3.14;          // float (double)
auto s = "hello";       // string
auto c = 'x';           // char
auto b = true;          // bool
auto n = null;          // null
print(type_of(i), " ", type_of(f), " ", type_of(s), " ", type_of(c), " ", type_of(b), " ", type_of(n));
print("hex: {}, binary: {}, exponent: {}", 0xFF, 0b1010, 2.5e3);
i + f;
output
int float string char bool null
hex: 255, binary: 10, exponent: 2500.0
== result: 45.14

Integers are 64-bit (int64), floats are doubles. Hex (0xFF) and binary (0b1010) integer literals work, as do exponent floats (2.5e3). Strings support the usual escapes (\n, \", \\) plus \xNN byte escapes (see Containers & Strings). A raw string, C++'s R"(...)", takes its text exactly as written, backslashes and quotes included and across lines (each line ending inside is one \n): R"(C:\games\save.dat)" is that path and R"(\d+)" a regex pattern, and a delimiter of up to 16 characters between the quote and the parenthesis lets the text hold )": R"js(say("hi"))js". Which rung each of these declarations sits on (and why auto here is not "dynamic") is the next chapter.

Comments and the implicit result​

jaiscript
// single-line comment
/* multi-line
   comment */
auto x = 10; // trailing comment
x * 2;  // the last expression's value is what execute() returns
output
== result: 20

A bare engine ships a basic print (arguments concatenate; no {} substitution); jai::stdlib::register_all upgrades it in place to the formatted version shown below and registers format. Both are variadic host functions: the canonical example of variadics, which live on the host side (see Functions & Lambdas).

jaiscript
print("plain string");
print("several", " ", "args", " ", "concatenate");
print("sequential: {} + {} = {}", 2, 3, 2 + 3);
print("positional: {1} before {0}", "cart", "horse");
print("literal {{braces}} in a formatted call: {}", true);
auto msg = format("format() returns: {}", 99);
print(msg);
print("no newline ", skip_newline);
print("continues the line");
output
plain string
several args concatenate
sequential: 2 + 3 = 5
positional: horse before cart
literal {braces} in a formatted call: true
format() returns: 99
no newline continues the line
== result: null

Rules:

  • print(value). One argument: printed as-is (no {} processing).
  • print(fmt, args...). If the first argument contains {} or {n} placeholders, it is treated as a format string: {} consumes the next argument, {0}/{1} are positional (0-based over the args after the format string), {{/}} escape braces.
  • print(a, b, c). If the first argument has no placeholders, all arguments are concatenated.
  • Trailing skip_newline suppresses the newline; skip_flush suppresses newline and flush.
  • Every argument prints its display text, as to_string(x) and "text" + x show it: a container its contents, an instance its class’s string to_string() or else its class name and fields (Printing values). A container’s or an instance’s text is never read as a format string.
  • format(...) follows the same rules but returns the string instead of printing.

Note: brace escaping ({{) only applies on the format path (multiple arguments with placeholders). A single-argument print("{{x}}") prints the braces literally.

Stringifying values​

to_string(x) converts any value to a string; a float prints as the shortest text that reads back to the same value, keeping .0 on a whole number (3.14 → "3.14", 2.0 → "2.0", 0.1 + 0.2 → "0.30000000000000004"), so to_string(x).to_float() == x for every number (infinities print inf and -inf, and every NaN prints nan on every platform); a format spec such as ${x:.2f} picks a fixed width when you want one. A container prints its contents, strings inside it quoted (Printing values); to_json(x) writes the archive text that from_json reads back:

jaiscript
auto nums = [1,2,3];
print(nums);
print({"a": 1, "b": ["x"]});
print(to_json(nums));
print(to_json({"a": 1}));
output
[1, 2, 3]
{"a": 1, "b": ["x"]}
[1,2,3]
{"a":1}

type_of(x) answers null, bool, int, float, char, string, utf8_string, array, map, function or weak_ptr. A class instance answers its class's name, and so does a shared_ptr handle to one (Foo, math::vec2, time::stopwatch); a reference answers the type of what it refers to; a coroutine handle answers object. A typed array is still array: its element type lives in its declaration.

jaiscript
class Foo { }
var handle = new Foo();
int x = 1;
auto& r = x;
array<int> xs = [1];
print(type_of(Foo()), " ", type_of(handle), " ", type_of(r), " ", type_of(xs), " ", type_of(math::vec2(1, 2)));
output
Foo Foo int array math::vec2