Write a story and run it anywhere
{
print("Would you like to write a simple story book?")
choice("Yes", {
print("Great! Let's check this out!")
})
choice("No", {
print("No worries!")
})
}
Stories that are written in JABL can be run from the command line, a web server or a web browser.
JABL is a constrained language that allows you to write stories and run them equally well from a CLI, web server or a web browser.
Each section of a story is a single file in a directory, where the directory is the story itself. Every story must have an entrypoint.jabl file which serves as the starting point.
$ tree
.
├── story
│ ├── entrypoint.jabl # Required - marks this as a story root
│ ├── chapter1.jabl
│ └── chapter2.jablThe entrypoint.jabl file is special: it identifies a directory as a story root. Section references (like goto("chapter1.jabl")) are resolved relative to the story root.
There are only a handful of special keywords in JABL.
If something can be achieved without introducing a new keyword, it should be done.
print: Print a message to the consolechoice: Present a choice to the usergoto: Jump to another sectionif: Choose the branch to run based on a conditionset: Set a variableget,getn,getb: Get a variable (string, number, boolean)
Adds text to the output buffer.
print("Hello, World!")
Present a choice to the user. The first parameter is the identifier of this choice and the second parameter is a block of code to execute when the user selects it.
choice("Yes", {
print("Great! Let's check this out!")
})
Jump to another section. This causes the interpreter to load and execute the section with the given identifier.
It is up to the loader itself to determine the code for a particular identifier.
goto("chapter1.jabl")
Choose the branch to run based on a condition. If accepts any expression that evaluates to a boolean and must be wrapped in parentheses.
if (true) {
print("this is printed")
}
You can also use an else statement
if (false) {
print("this is not printed")
} else {
print("this is printed")
}
Set a variable. The first parameter is the name of the variable and the second parameter is the value.
This function returns the value that was set.
Variables can be float64 values, strings or booleans.
set("some-value", 123) # => 123
set("some-value", true) # => true
set("some-value", "test") # => "test"
Get a variable. The first parameter is the name of the variable.
This function returns the value of the variable.
This returns the string value or "" if the variable does not exist.
get("some-value") # => "Hello World!"
Get a variable. The first parameter is the name of the variable.
This function returns the value of the variable.
This returns the float64 value or 0 if the variable does not exist.
get("some-value") # => 123
Get a variable. The first parameter is the name of the variable.
This function returns the value of the variable.
This returns the bool value or false if the variable does not exist.
getb("some-value") # => false
You can add single-line coments by prefixing a line with //.
// This is a comment
The JABL interpreter is a simple program that reads a story and executes it. It is written in Go and can be run from the command line or compiled to WASM and invoked from Javascript.
The Interpreter has two key abstractions:
StateMapper: Implements thegetandsetfunctions with a backing store forfloat64variablesSectionLoader: Implements a strategy for loading sections from a story. This could be from a URL or a local file system.
It is (should be) safe to load JABL code from any source, as the interpreter does not execute any code until it has been parsed and validated.
The interpreter reads a section by identifier from a SectionLoader, then parses the JABL code into an AST. The AST is then executed by the interpreter.
The JABL code is executed immediately and synchronously and results in a structure that has three components:
output: A string that is printed to the consolechoices: A list of choices that the user can maketransition: The identifier of the next section to load and execute
If there is a transition value, the interpreter will load and execute the next section. If there is no transition value, the interpreter will stop until the user selects a choice.
Each choice is a name and function that is executed when the user selects it. The function is a block of JABL code that is executed immediately and synchronously.
The output is an output buffer that is a collection of each print statement separated by a newline.
The JABL interpreter can be built for the command line or for the web.
go build -o bin cmd/cli/main.goGOOS=js GOARCH=wasm go build -o ./web/jabl.wasm ./cmd/wasm/main.goWhen upgrading TinyGo, update wasm-init.js to match the new version:
cp "$(tinygo env TINYGOROOT)/targets/wasm_exec.js" web/src/wasm-init.jscd web
npm ci
npm run startThe CLI includes a validator with static analysis to catch errors before runtime:
# Validate a single story
./bin/cli validate assets/my-story/
# Validate all stories under a directory
./bin/cli validate assets/
# Example output with enhanced diagnostics
FAIL assets/my-story/chapter1.jabl
E001 line 4:27
variable "heath" read but never set
4 | print("Health: " + getn("heath"))
^^^^^ did you mean "health"?
OK assets/my-story/entrypoint.jablThe validator automatically detects story roots by looking for entrypoint.jabl files. When validating a parent directory like assets/, each story is validated independently with its own section namespace.
The validator checks for:
- Syntax errors - missing braces, invalid tokens
- Undefined variables - reading variables that were never set
- Type mismatches - setting a variable as one type and reading as another
- Typo detection - suggests similar variable names when a typo is detected
- Missing sections - goto targets that don't exist in the story directory
Enhanced output includes line numbers, source context, and visual highlighting.