# The formal grammar

> EBNF, reserved words and lexical rules.

Source: https://algobarsx.com/docs/grammar/

The formal grammar, for anyone building tooling such as a syntax highlighter or a linter. Notation is ISO/IEC 14977 style EBNF. The parser implements this grammar exactly, and every example on this page parses with zero diagnostics.

```ebnf
(* ═══════════════════════════════════════════════════════════════════════════════
   AlgoBarsX v1 — formal grammar (Phase 0)
   ───────────────────────────────────────────────────────────────────────────────
   Notation: ISO/IEC 14977 style EBNF.  "x" = terminal,  [ ] = optional,  { } = repeat,
   ( | ) = choice.  UPPER-CASE names are tokens produced by the lexer (src/lexer/lexer.ts).
   The parser (src/parser/parser.ts) implements this grammar exactly; the acceptance scripts in
   acceptance/*.algo must parse with zero diagnostics.
   ═══════════════════════════════════════════════════════════════════════════════ *)

(* ─── 1. Lexical structure ─────────────────────────────────────────────────────── *)

(* Logical lines.  A NEWLINE ends a statement.  Line breaks inside ( ), [ ] or { } are
   ignored, so expressions may span lines there.  Blank lines and comment-only lines
   produce no tokens.  Indentation (spaces; tabs are an error) produces INDENT when a
   line is indented deeper than the enclosing block and one DEDENT per closed block. *)

NEWLINE   = ? end of a logical line ? ;
INDENT    = ? start of a deeper indentation level ? ;
DEDENT    = ? return to an enclosing indentation level ? ;
EOF       = ? end of input ? ;

comment   = "#" , { ? any character except line break ? } ;
            (* "#" followed by exactly 6 or 8 hex digits and a non-name character is a COLOR,
               except at the start of a line, where "#" always begins a comment. *)

IDENT     = letter , { letter | digit | "_" } ;          (* case-sensitive *)
letter    = "A" | … | "Z" | "a" | … | "z" | "_" ;
digits    = digit , { [ "_" ] , digit } ;                  (* 1_000_000 *)

NUMBER    = digits , [ "." , digits ] , [ ( "e" | "E" ) , [ "+" | "-" ] , digits ] ;
PERCENT   = NUMBER , "%" ;                                 (* no space: 1%  0.5% *)
RMULT     = NUMBER , "R" ;                                 (* no space: 2R  1.5R *)
MONEY     = "$" , NUMBER ;                                 (* $500 *)
DURATION  = digits , ( "s" | "m" | "h" | "d" | "w" | "M" ) ; (* 30s 15m 4h 1d 1w 1M — also timeframes *)
ORDINAL   = digits , ( "st" | "nd" | "rd" | "th" ) ;       (* 4th — only inside rule modifiers *)
TIME      = digit , [ digit ] , ":" , digit , digit ;      (* 08:00 *)
DATE      = digit , digit , digit , digit , "-" , digit , digit , "-" , digit , digit ;   (* 2026-01-15 *)
COLOR     = "#" , hex6 , [ hex2 ] ;                        (* #22c55e  #22c55e80 *)
STRING    = '"' , { character | escape | "{{" | "}}" | interpolation } , '"' ;
escape    = "\" , ( '"' | "\" | "n" | "t" ) ;
interpolation = "{" , expression , [ ":" , format ] , "}" ;   (* "RSI {rsi(close, 14):0.0}" *)

UNIT_WORD = "pips" | "pip" | "points" | "point" | "lots" | "lot" | "bars" | "bar" ;
            (* a NUMBER followed by a UNIT_WORD is one unit value: 20 pips, 10 bars *)

(* Reserved words — cannot be used as names:
     strategy indicator alert library use as input const state export type enum fn action
     return if elif else for in match when on and or not then between of true false na
     break continue confirmations sequence
   Reserved words MAY be used after "." (ribbon.on), as named-argument names (on: gold)
   and as option names (step: 0.1%).  Every other word, such as every, cooldown, session,
   bar, max, from or step, is contextual and remains usable as a name. *)

(* ─── 2. Program and header ────────────────────────────────────────────────────── *)

program       = { NEWLINE } , [ version_line ] , header , { statement } , EOF ;
version_line  = "algobarsx" , NUMBER , NEWLINE ;
header        = script_kind , STRING , NEWLINE , [ INDENT , setting , { setting } , DEDENT ] ;
script_kind   = "strategy" | "indicator" | "alert" | "library" ;
setting       = name , ":" , setting_item , { "," , setting_item } , NEWLINE ;
setting_item  = expression , { expression } ;   (* juxtaposed words: once per bar close, every 1m *)
name          = IDENT | reserved_word ;

(* ─── 3. Statements ────────────────────────────────────────────────────────────── *)

statement     = use_stmt | input_stmt | const_stmt | state_stmt | export_stmt | enum_stmt
              | type_decl | fn_decl | if_stmt | for_stmt | match_stmt | when_stmt | on_stmt
              | confirmations | sequence | command | assignment | call_stmt
              | return_stmt | "break" , NEWLINE | "continue" , NEWLINE ;

block         = NEWLINE , INDENT , statement , { statement } , DEDENT
              | statement ;                       (* one statement on the same line *)

use_stmt      = "use" , ( "indicator" | "library" ) , STRING , [ VERSION ] , "as" , IDENT ,
                [ "(" , [ arguments ] , ")" ] , NEWLINE ;
VERSION       = ? an IDENT of the form v<digits>, such as v3 ? ;

input_stmt    = "input" , IDENT , "=" , expression , { "," , option } , NEWLINE ;
const_stmt    = "const" , IDENT , [ ":" , type ] , "=" , expression , NEWLINE ;
state_stmt    = "state" , IDENT , [ ":" , type ] , "=" , expression , NEWLINE ;
export_stmt   = "export" , IDENT , "=" , expression , NEWLINE ;
enum_stmt     = "enum" , IDENT , ":" , IDENT , { "," , IDENT } , NEWLINE ;

type_decl     = "type" , IDENT , ":" , NEWLINE , INDENT , field , { field } , DEDENT ;
field         = IDENT , ":" , type , [ "=" , expression ] , NEWLINE ;
type          = name , [ "<" , type , { "," , type } , ">" ] ;     (* number, list<Level>, map<string, number> *)

fn_decl       = [ "action" ] , "fn" , IDENT , "(" , [ param , { "," , param } ] , ")" ,
                [ "->" , type ] , ":" , block ;
param         = IDENT , ":" , type , [ "=" , expression ] ;

assignment    = IDENT , ":" , type , "=" , expression , NEWLINE          (* typed declaration *)
              | target , assign_op , expression , NEWLINE ;
target        = postfix ;                         (* must be a name, a field or an item *)
assign_op     = "=" | "+=" | "-=" | "*=" | "/=" ;
call_stmt     = postfix , NEWLINE ;               (* must end in a call: levels.push(x) *)
return_stmt   = "return" , [ expression ] , NEWLINE ;

if_stmt       = "if" , expression , ":" , block ,
                { "elif" , expression , ":" , block } , [ "else" , ":" , block ] ;
for_stmt      = "for" , IDENT , "in" , expression , ":" , block ;     (* for i in 0..50 / for z in zones *)
match_stmt    = "match" , expression , ":" , NEWLINE , INDENT , match_arm , { match_arm } , DEDENT ;
match_arm     = expression , ":" , block ;

(* ─── 4. Rules, events, confirmations and sequences ────────────────────────────── *)

when_stmt     = "when" , expression , [ "as" , IDENT ] , { modifier } , ":" , block ;
modifier      = "every" , ( ORDINAL | "bar" )                 (* every 4th · every bar *)
              | "skip" , "first" , NUMBER                     (* skip first 3 *)
              | "max" , NUMBER , "per" , IDENT                (* max 2 per day *)
              | "cooldown" , ( DURATION | NUMBER , UNIT_WORD )  (* cooldown 30m · cooldown 5 bars *)
              | "once" , "per" , "bar"                        (* once per bar *)
              | "within" , ( "session" | "sessions" ) , IDENT , { "," , IDENT }
              | "from" , TIME , "to" , TIME , [ zone ] ;      (* from 08:00 to 11:00 Europe/London *)
zone          = IDENT , { "/" , IDENT } ;

on_stmt       = "on" , IDENT , { IDENT } , [ "(" , IDENT , ")" ] , [ STRING ] , ":" , block ;
                (* on start · on bar close · on exit(trade) · on session open "new_york" · on render(canvas) *)

confirmations = "confirmations" , IDENT , ":" , NEWLINE , INDENT ,
                confirm_line , { confirm_line } , DEDENT ;
confirm_line  = IDENT , ":" , expression , NEWLINE
              | "require" , ":" , ( "all" | "any" | "at" , "least" , NUMBER ) , NEWLINE ;

sequence      = "sequence" , IDENT , [ "within" , ( NUMBER , UNIT_WORD | DURATION ) ] , ":" , NEWLINE ,
                INDENT , sequence_line , { sequence_line } , DEDENT ;
sequence_line = "step" , IDENT , ":" , expression , NEWLINE
              | "reset_if" , ":" , expression , NEWLINE ;

(* ─── 5. Commands (orders, management, notifications, drawing) ─────────────────── *)

command       = COMMAND_WORD , [ command_items ] , ( NEWLINE | ":" , NEWLINE , INDENT , statement , { statement } , DEDENT ) ;
command_items = command_item , { [ "," ] , command_item } ;   (* the comma may be omitted before an option *)
command_item  = option | expression , [ "as" , IDENT ] ;      (* positional values come before options *)
option        = name , ":" , expression ;

COMMAND_WORD  = "buy" | "sell" | "close" | "close_all" | "modify" | "cancel" | "notify" | "log"
              | "partial" | "breakeven" | "trail" | "exit"
              | "plot" | "fill" | "hline" | "mark" | "label" | "line" | "box" | "table" | "bar_color"
              | "background" | "pane" | "polygon" | "polyline" | "curve" | "channel" | "profile"
              | "heatmap" | "cells" | "candles" | "fib" | "pitchfork" | "arrow" | "icon" | "image"
              | "tooltip" | "dashboard" ;
              (* A command word starts a command only when the next token can start a value or ends the
                 line; "close > open" is an expression, "close trade" is a command.  "plot (a + b) / 2"
                 is a command because of the space before "(". *)

(* ─── 6. Expressions (lowest to highest precedence) ─────────────────────────────── *)

expression    = lambda | conditional | or_expr ;
lambda        = ( IDENT | "(" , [ IDENT , { "," , IDENT } ] , ")" ) , "=>" , expression ;
conditional   = "if" , expression , "then" , expression , "else" , expression ;
or_expr       = and_expr , { "or" , and_expr } ;
and_expr      = not_expr , { "and" , not_expr } ;
not_expr      = "not" , not_expr | comparison ;
comparison    = range_expr , [ ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) , range_expr
                             | "in" , range_expr
                             | "between" , range_expr , "and" , range_expr ] ;   (* non-associative *)
range_expr    = additive , [ ".." , additive ] ;
additive      = of_expr , { ( "+" | "-" ) , of_expr } ;
of_expr       = multiplicative , [ "of" , multiplicative ] ;                     (* 2% of account.balance *)
multiplicative= unary , { ( "*" | "/" | "%" ) , unary } ;
unary         = ( "-" | "+" ) , unary | power ;
power         = postfix , [ "**" , unary ] ;                                     (* right-associative *)
postfix       = primary , { "(" , [ arguments ] , ")" | "[" , expression , "]" | "." , name } ;
arguments     = argument , { "," , argument } ;
argument      = [ name , ":" ] , expression ;

primary       = NUMBER , [ UNIT_WORD ] | PERCENT | RMULT | MONEY | DURATION | TIME | DATE | COLOR | STRING
              | "true" | "false" | "na" | IDENT
              | "(" , expression , ")"
              | "(" , expression , "," , expression , { "," , expression } , ")"   (* tuple: (bar, price) *)
              | "[" , [ expression , { "," , expression } ] , "]" ;                (* list *)
```
