Documentation
Bop language guide

Grammar

Grammar🔗

An informal grammar for the Bop language, plus the complete list of reserved words.

Reserved words🔗

let const pub fn return
if else while for in repeat break continue
use as struct enum match try
true false none

These can’t be used as variable or function names. There are no word-spelled logical operators — use &&, ||, ! rather than and, or, not.

Built-in functions🔗

Always in scope; can’t be shadowed by user fns. See Built-in Functions for details.

FunctionReturnsDescription
print(args...)noneHost-captured output
range(n) / range(s, e) / range(s, e, step)arrayInteger range
rand(n)intPseudo-random 0..n
try_call(f)ResultRun f, return Ok(v) or Err(RuntimeError)
panic(message)never returnsRaise a non-fatal runtime error carrying message

All math and conversion operations are methods on values: x.type(), x.to_str(), x.to_int(), x.to_float(), x.abs(), a.min(b), x.sqrt(), x.len(), etc.

Grammar🔗

Informal EBNF. Whitespace is insignificant except for newlines, which auto-insert semicolons (see below).

program     = statement*
statement   = letDecl | constDecl | assign | ifStmt | whileStmt | repeatStmt
            | forStmt | fnDecl | returnStmt | breakStmt | continueStmt
            | useStmt | structDecl | enumDecl | methodDecl
            | exprStmt

letDecl     = "let" IDENT "=" expr
constDecl   = "const" IDENT "=" expr
assign      = target ("=" | "+=" | "-=" | "*=" | "/=" | "%=") expr
target      = IDENT | postfix "[" expr "]" | postfix "." IDENT

ifStmt      = "if" expr block ("else" "if" expr block)* ("else" block)?
whileStmt   = "while" expr block
repeatStmt  = "repeat" expr block
forStmt     = "for" IDENT "in" expr block
fnDecl      = "pub"? "fn" IDENT "(" params? ")" block
returnStmt  = "return" expr?
breakStmt   = "break"
continueStmt = "continue"

useStmt     = "use" path
            | "use" path "." "{" IDENT ("," IDENT)* "}"
            | "use" path "as" IDENT
            | "use" path "." "{" IDENT ("," IDENT)* "}" "as" IDENT
path        = IDENT ("." IDENT)*

structDecl  = "struct" IDENT "{" fields? "}"
fields      = IDENT ("," IDENT)*
enumDecl    = "enum" IDENT "{" variants? "}"
variants    = variant ("," variant)*
variant     = IDENT                                             // unit
            | IDENT "(" IDENT ("," IDENT)* ")"                  // tuple
            | IDENT "{" IDENT ("," IDENT)* "}"                  // struct
methodDecl  = "fn" IDENT "." IDENT "(" params ")" block

exprStmt    = expr
block       = "{" statement* "}"

expr        = or
or          = and ("||" and)*
and         = equality ("&&" equality)*
equality    = comparison (("==" | "!=") comparison)*
comparison  = addition (("<" | ">" | "<=" | ">=") addition)*
addition    = multiply (("+" | "-") multiply)*
multiply    = unary (("*" | "/" | "%") unary)*
unary       = ("!" | "-" | "try") unary | postfix
postfix     = primary (call | index | field | method | structLit | variantCtor)*
call        = "(" args? ")"
index       = "[" expr "]"
field       = "." IDENT
method      = "." IDENT "(" args? ")"
structLit   = "{" (IDENT ":" expr ("," IDENT ":" expr)*)? "}"   // only at expr position
variantCtor = "::" IDENT payload?
payload     = "(" expr ("," expr)* ")"                          // tuple variant
            | "{" IDENT ":" expr ("," IDENT ":" expr)* "}"      // struct variant

primary     = INT | NUMBER | STRING | "true" | "false" | "none"
            | IDENT | resultShorthandExpr | "(" expr ")" | arrayLit | dictLit
            | ifExpr | matchExpr | fnExpr

resultShorthandExpr = ("Ok" | "Err") "(" expr ("," expr)* ")"  // sugar for Result::Ok/Err

arrayLit    = "[" (expr ("," expr)* ","?)? "]"
dictLit     = "{" (STRING ":" expr ("," STRING ":" expr)* ","?)? "}"
ifExpr      = "if" expr "{" expr "}" "else" "{" expr "}"
matchExpr   = "match" expr "{" arm ("," arm)* ","? "}"
arm         = pattern ("if" expr)? "=>" expr
fnExpr      = "fn" "(" params? ")" block

pattern     = orPattern
orPattern   = singlePattern ("|" singlePattern)*
singlePattern = "_" | IDENT | literal | variantPattern | structPattern | arrayPattern
              | resultShorthand
variantPattern = (IDENT ".")? IDENT "::" IDENT
              | (IDENT ".")? IDENT "::" IDENT "(" pattern ("," pattern)* ")"
              | (IDENT ".")? IDENT "::" IDENT "{" IDENT ":" pattern ("," IDENT ":" pattern)* "}"
resultShorthand = ("Ok" | "Err") "(" pattern ("," pattern)* ")"  // sugar for Result::Ok/Err
structPattern  = (IDENT ".")? IDENT "{" IDENT ":" pattern ("," IDENT ":" pattern)* "}"
arrayPattern   = "[" patternList? arrayRest? "]"
patternList    = pattern ("," pattern)*
arrayRest      = ".." | ".." IDENT

params      = IDENT ("," IDENT)*
args        = expr ("," expr)*

INT is an exact signed 64-bit integer after unary parsing. Decimal magnitudes through 9223372036854775807 are ordinary primary expressions; the boundary spelling -9223372036854775808 is accepted when unary - directly owns that magnitude, including in literal patterns. A bare 9223372036854775808, 0 - 9223372036854775808, or any larger magnitude is out of range rather than being converted to a floating-point number.

Note: pub is accepted only on a named fn at the direct program root; it marks an entry for the stateful embedding ABI. It is not valid on methods, function expressions, or declarations nested in a block or callable.

methodDecl, enum variant IDENTs, and struct names must start with an uppercase letter. IDENT bound by let, fn, parameters, for, etc. must start with lowercase or _. const names must be all-caps. Mis-shaped declarations parse-error with a “did you mean?” suggestion — see Variables.

Automatic semicolons🔗

Bop automatically inserts a semicolon at the end of a line if the last token is one of:

  • An identifier or literal (int, number, string)
  • true, false, none
  • break, continue, return
  • ), ], }

Newlines do not insert semicolons while the innermost open delimiter is ( or [. This makes calls, conditions, array literals, and index expressions safe to lay out over multiple lines. A newline immediately before a closing ), ], or } is also ignored, so the final item in a multiline literal does not require a trailing comma.

Braces remain statement-capable even when their block is nested inside parentheses or brackets. Newlines between statements in a function or control-flow block still insert semicolons:

let callbacks = [
  fn() {
    let x = 1
    return x
  },
]

A line starting with . continues a preceding value, including across blank lines or comments:

let size = values
  // Continue the same expression.
  .len()
  .to_str()

return itself remains a semicolon trigger. A newline immediately after it therefore means a bare return whose value is none; put the value on the same line, or open a parenthesized expression on that line when it needs multiline layout:

return (
  left +
  right
)

This means the opening { of a block must be on the same line as its keyword:

// Correct
if x > 3 {
  print("yes")
}

// Wrong — semicolon inserted after "3"
if x > 3
{
  print("yes")
}

You can also separate statements on the same line with an explicit ;:

let x = 1; let y = 2

Comments🔗

// starts a line comment — everything to the end of line is ignored:

// Whole-line comment
let x = 5   // Inline trailing comment

There’s no block-comment syntax.

String interpolation🔗

Inside double-quoted strings, {identifier} inserts the value of a variable. Only plain variable names are allowed — no expressions, operators, or function calls:

let name = "Alice"
print("Hello, {name}!")     // works
// print("Hello, {1 + 2}!")  // error — expressions not allowed

Use \{ and \} for literal braces in strings. Other supported escapes: \", \\, \n, \t, \r.

Found something unclear?Edit this page on GitHub
Documentation

Search Bop

Start typing to search the guide and reference.