Hello & Basics​

JaiScript is an embedded scripting language that reads like C++: typed or dynamic variables, value semantics, classes, lambdas, and a static checker that reads the whole program before its first statement runs. A host program embeds it as a library and can reload scripts while it runs.

One engine runs a script on one of two backends: a bytecode VM (the default) and a tree-walking interpreter held to the same behavior. Every standalone sample on this site was run on both, and the output shown is what both printed. The one documented difference is recursion depth: a deep non-tail recursion that returns on the VM can stop on the interpreter with Native stack exhausted (Recursion).

Running a script from C++​

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

auto eng = jai::engine::make();                 // shared_ptr<engine>, runs the VM
jai::stdlib::register_all(eng);                 // formatted print, format, to_string, type_of, to_json, ...
int v = eng->execute<int>("2 + 2");            // 4
  • The value of the last expression is the result of execute(); top level needs no return. execute<T>() hands it back as a T and throws when it is not one.
  • The checker runs first and is strict: a type mistake it can see, or an undeclared name, throws out of execute() before any statement runs. Chapter 13 has the whole story, including the [[warn_mode]] attribute that turns a region's errors into warnings.
  • eng->set_backend(jai::backend_type::interpreter) selects the interpreter. Call it before the first execute() (later is an error) and link jaiscript_interpreter.

Try it from a terminal​

The guide needs no host program. The build produces a standalone runner, jaiscript; the installer in Get JaiScript puts it on your PATH, and in a clone it is one CMake target (named jaiscript_runner, because the library owns the name jaiscript):

shell
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release --target jaiscript_runner   # build/bin/jaiscript

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
FlagWhat it does
--backend=vm|interpWhich backend runs the script. The default is vm.
--warningsLists the checker's warnings before the script runs. Errors always fail the load and print.
--budget=NSeconds of wall clock per execute; an overrun is a terminal error no catch sees (Control Flow). Default 0: unlimited.

The runner's defaults are not the engine's. An embedded jai::engine enforces a 5.0 second execution budget per execute(); the runner has none. Both start on the VM, and checking is strict in both.

The REPL runs each entry as one execute() on a persistent engine. An entry's last statement needs no ; (int hp = 100, hp += 1, import "rules.jai"); an expression prints its value, and a declaration prints nothing. After a file run the runner prints no result (wrap the last expression in print(...) to see it), and an int result becomes the exit code.

Output blocks on these pages follow three conventions:

  • == result: … is the value of the script's last expression, what execute() hands a host. The lines above it are print output.
  • == error: … is the message of an uncaught error. The runner prints it to stderr and exits 1.
  • A block that starts jaiscript: static check failed: or jaiscript: parse failed: is the runner's listing when a load stops before anything runs.

Values and literals​

jaiscript
// a line comment
/* a block
   comment */
auto i = 42;            // int: 64-bit
auto f = 3.14;          // float: a double
auto s = "hello";       // string
auto c = 'x';           // char: one byte
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);
print(R"(C:\games\save.dat)", " ", R"js(say("hi"))js");
i + f;
output
int float string char bool null
hex: 255, binary: 10, exponent: 2500.0
C:\games\save.dat say("hi")
== result: 45.14
  • Numbers. Integer literals are decimal, hex (0xFF) or binary (0b1010). Floats take C++'s spellings: an exponent (2.5e3), or digits on one side of the point alone (1., .5, 1.e5).
  • Strings take the escapes \n, \r, \t, \", \', \\ and \xNN, a byte as exactly two hex digits (Bytes and chars).
  • Raw strings, C++'s R"(...)", take their text as written, backslashes and quotes included, and across lines (each line ending is one \n). A delimiter of up to 16 characters between the quote and the parenthesis lets the text hold )".
  • auto infers the variable's type once and keeps it. The next chapter covers auto, typed declarations and the dynamic var.

jai::stdlib::register_all registers the formatted print, plus format and vformat. (A bare engine's print only concatenates its arguments.)

jaiscript
print("several", " ", "args", " ", "concatenate");
print("sequential: {} + {} = {}", 2, 3, 2 + 3);
print("positional: {1} before {0}", "cart", "horse");
print("{:.2f} and {{braces}}", 3.14159);
string typed = "a user typed {}";
print(typed, " and it stays as typed");
string pattern = "{} of {}";
print(vformat(pattern, 1, 2), " / ", format("{} hp", 99));
print("no newline ", skip_newline);
print("continues the line");
output
several args concatenate
sequential: 2 + 3 = 5
positional: horse before cart
3.14 and {braces}
a user typed {} and it stays as typed
1 of 2 / 99 hp
no newline continues the line
CallResult
print("fmt {} {0}", a, b)A string literal written first that holds placeholders is a format string: {} takes the next argument, {n} argument n (from 0), {{ and }} are braces, and {:spec} formats (format specs).
print(a, b, c)Any other first argument (a variable, an element, a concatenation, text a user typed) is text: the arguments concatenate, and braces print as they are. print("{{x}}"), one argument, prints {{x}}.
format("fmt", args...)The same rules, returning the string.
vformat(fmt, args...)Formats a text known only at run time, as C++'s std::vformat does.
trailing skip_newline / skip_flushNo newline / no newline and no flush.
  • Placeholders and arguments must pair up. For a literal the count is checked at load: the format text of 'print' has 2 placeholder(s) for 1 argument(s), and a literal alone with a placeholder (print("{}")) has none to take. vformat checks when it runs: Format text has 2 placeholder(s) for 1 argument(s).
  • format with a first argument the checker knows is a string but not a literal fails the load: the format text of 'format' must be a string literal: vformat(fmt, ...) formats a run-time text, and + or a template string joins text.
  • Only a call that names print or format formats: a function value holding print takes every argument as text.
  • Every argument prints its display text, the text to_string(x) gives (Printing values). A container or an object is never read as a format string.

to_string, to_json and type_of​

jaiscript
print(to_string(2.0), " ", to_string(0.1 + 0.2), " ", "3.5".to_float() + 1);
print([1, "two"], " ", {"a": 1});       // display text: strings inside a container are quoted
print(to_json([1, "two"]), " ", to_json({"a": 1}));
class Foo { }
int x = 1;
auto& r = x;
print(type_of(Foo()), " ", type_of(r), " ", type_of([1]));
output
2.0 0.30000000000000004 4.5
[1, "two"] {"a": 1}
[1,"two"] {"a":1}
Foo int array
  • to_string(x) gives any value's display text. A float prints as the shortest text that reads back to the same value, with .0 kept on a whole number, so to_string(x).to_float() == x. Infinities print inf and -inf, and every NaN prints nan.
  • to_json(x) writes the JSON text that from_json reads back (JSON).
  • type_of(x) answers null, bool, int, float, char, string, utf8_string, array, map, function, weak_ptr or coroutine_handle; later chapters introduce the kinds not met yet. A class instance, or a shared handle to one, answers its class's name (Foo, math::vec2). A reference answers the type it refers to, and a typed array is still array.