Language Reference

Formal reference for the plotknot language. For a tutorial-style introduction, see the Language Guide.

Document structure

A plotknot document is a sequence of sections. Each section begins with a heading and contains statements.

# Section Title

statements...

## Another Section

statements...

Imports

Stories can be split across multiple files:

import characters
import "scenes/intro.plotknot"

Statements

Text

Any line that doesn't start with a keyword or special character:

This is shown to the player.

Text lines can carry a localization key (a trailing identifier):

Hello, world! greeting_key

Interpolation

Text supports variable and expression interpolation:

Hello $name!
Your score is ${score + 10} points.

Interpolation is also valid in choice text.

Inline tags

Text lines support inline tags for styling metadata:

<friendly> Welcome to the shop!
<wave size=2> Hello there!
<mood=angry> Grr!

Choice block

choice:
- Display text -> target_section
- Conditional text -> target [if expression]
-- Dismissible text -> target
- Nested text -> target
  set x = 1
  Transition text here.

Set (variable assignment)

set variable = expression

Call (game interop)

call function_name(arg1, arg2) -> result_var
call function_name(arg1)

If/Else

if expression:
  statements...
else:
  statements...

Goto (navigation)

-> target_section

Comment

// This is ignored by the compiler

Variation block

cycle
  First visit text.
--
  Second visit text.
--
  Third and later visit text.
Mode Behavior
sequence Play items in order, stick on last
cycle Play items in order, loop back to first
once Play items in order, then produce nothing
pick Choose a random item each time
shuffle Play all items in random order, then reshuffle

Declare (variable declaration)

declare score = 0
declare name = "Ada"
declare has_key = false

Stop (story termination)

stop

Function declaration (external functions)

function name(param_count)

Expressions

Precedence (lowest to highest)

Level Operators Associativity
1 or left
2 and left
3 not right (unary)
4 == != < > <= >= left
5 + - left
6 * / left
7 primary (literals, identifiers, calls, parens)

Literals

Type Examples
Integer 42, 0, -1
Float 3.14, 0.5
String "hello", "world"
Boolean true, false

Identifiers

Variable references: gold, player_name, is_alive.

Dot access (character properties)

barista.friendship
keeper.mood
  set barista.friendship = barista.friendship + 1
  if keeper.mood == "happy":
    ...

Function calls

function_name(arg1, arg2, ...)

Functions can appear in any expression position.

Grouping

Parentheses override precedence:

set result = (a + b) * c

Metadata

Key-value pairs at the top of a section body (before any statements):

# Section

key: value
another_key: another value

First text line here.

Metadata is included in the compiled output as a table.

Keywords

Keyword Context Purpose
choice: statement Begin a choice block
set statement Assign a variable
call statement Invoke a game function
if statement/expression Conditional branch
else statement Alternative branch
declare statement Declare a variable with an initial value
stop statement Explicitly terminate the story
function statement Declare an external function (runtime-provided)
import top-level Import sections from another file
sequence statement Variation mode: play in order, stick on last
cycle statement Variation mode: play in order, loop
once statement Variation mode: play in order, then nothing
pick statement Variation mode: random item each time
shuffle statement Variation mode: random order, reshuffle
and expression Logical conjunction
or expression Logical disjunction
not expression Logical negation
true expression Boolean literal
false expression Boolean literal

Special tokens

Token Purpose
# / ## Section heading
-> Navigation / choice target / call result
- Choice item marker
-- Dismissible choice marker / variation item separator
[if ...] Inline choice condition
// Comment
: Metadata separator / block opener
$ident Variable interpolation in text
${expr} Expression interpolation in text
$$ Literal dollar sign
<tag> Inline text tag
<tag prop=val> Inline text tag with property
. Character property access (char.prop)

Compiled output format

The compiler produces a Lua table:

return {
  start = "section_id",
  variables = {
    ["score"] = 0,
    ["name"] = "Ada"
  },
  sections = {
    ["section_id"] = {
      title = "Section Title",
      metadata = { ["key"] = "value" },
      lines = {
        -- Plain text
        { type = "text", text = "Hello" },
        { type = "text", text = "Bonjour!", key = "greeting_001" },

        -- Interpolated text (parts array)
        { type = "text", parts = {
          { type = "text", text = "You have " },
          { type = "expr", value = "vars.gold" },
          { type = "text", text = " gold." }
        }},

        -- Text with inline tags
        { type = "text", text = "Welcome!", tags = {
          { name = "friendly" }
        }},
        { type = "text", text = "Hello!", tags = {
          { name = "wave", props = { ["size"] = "2" } }
        }},

        -- Choice block (with dismissible and nested content)
        { type = "choice", choices = {
          { text = "Go", target = "other_section" },
          { text = "Secret", target = "secret", dismissible = true },
          { text = "Path", target = "path", lines = {
            { type = "set", var = "route", value = "forest" }
          }}
        }},

        -- Variable assignment
        { type = "set", var = "x", value = 5 },
        { type = "set", var = "y", value = "vars.x + 1", expr = true },

        -- Character property assignment (dot access)
        { type = "set", var = "barista.friendship",
          value = "chars.barista.friendship + 1", expr = true },

        -- Game interop call
        { type = "call", name = "fn", args = {1, 2}, result = "res" },

        -- Conditional block
        { type = "if", condition = "vars.x > 3",
          then_lines = { ... }, else_lines = { ... } },

        -- Navigation
        { type = "goto", target = "other_section" },

        -- Variation block
        { type = "variation", mode = "cycle", items = {
          { { type = "text", text = "First visit." } },
          { { type = "text", text = "Second visit." } }
        }},

        -- Story termination
        { type = "stop" }
      }
    }
  }
}

Key notes:

Grammar

document      = import* section+
import        = 'import' (IDENT | STRING) NEWLINE
section       = heading metadata* stmt*
heading       = ('#' | '##') TEXT NEWLINE
metadata      = IDENT ':' TEXT NEWLINE
stmt          = text | choice_block | set_stmt | call_stmt | if_block | goto
              | variation | declare_stmt | stop_stmt
text          = (TEXT | interp | tag)* ('#' IDENT)? NEWLINE
interp        = '$' IDENT | '${' expr '}'
tag           = '<' IDENT ('=' value)? '>'
choice_block  = 'choice' ':' NEWLINE choice+
choice        = ('-' | '--') ('[if' expr ']')? text '->' IDENT NEWLINE stmt*
set_stmt      = 'set' (IDENT | IDENT '.' IDENT) '=' expr NEWLINE
call_stmt     = 'call' IDENT '(' args? ')' ('->' IDENT)? NEWLINE
if_block      = 'if' expr ':' NEWLINE stmt* ('else' ':' NEWLINE stmt*)?
goto          = '->' IDENT NEWLINE
variation     = ('sequence' | 'cycle' | 'once' | 'pick' | 'shuffle') NEWLINE
                (stmt+ ('--' NEWLINE stmt+)*)?
declare_stmt  = 'declare' IDENT '=' expr NEWLINE
stop_stmt     = 'stop' NEWLINE
expr          = or_expr
or_expr       = and_expr ('or' and_expr)*
and_expr      = not_expr ('and' not_expr)*
not_expr      = 'not' not_expr | comparison
comparison    = primary (('==' | '!=' | '<' | '>' | '<=' | '>=') primary)?
primary       = IDENT | IDENT '.' IDENT | STRING | INT | FLOAT | BOOL
              | '(' expr ')' | call_expr
call_expr     = IDENT '(' args? ')'
args          = expr (',' expr)*