Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Welcome to Bop

Bop is a small, dynamically-typed programming language designed for teaching and embedding. It has a simple, modern syntax with no semicolons, no boilerplate, and no surprises — just the core concepts that matter. It runs in a sandbox with bounded resources and no access to the filesystem or network.

Bop isn’t Python, JavaScript, or any existing language, but it deliberately borrows familiar syntax so that skills transfer directly to real-world languages. Variables, loops, functions, arrays, dictionaries — real programming concepts, zero setup. The library has no dependencies and also supports no-std and wasm.

Quick example

// Sum the numbers from 1 to 10
let total = 0
for i in range(1, 11) {
  total += i
}
print("Sum: " + str(total))

What makes Bop different?

  • Built for learning — no semicolons, no boilerplate, simple syntax that teaches real programming concepts without getting in the way.
  • Looks like real code — curly braces, functions, operators. Very similar to modern languages so that skills are directly transferable.
  • Friendly errors — never cryptic, always helpful. ("I don't know what 'pritn' is — did you mean 'print'?")
  • Sandboxed by design — no imports, no file I/O, no network access. All resource usage is bounded.
  • Embeddable — zero dependencies, wasm support, and the BopHost trait for adding custom functions and controlling execution.

Where to start

If you’re new to Bop, start with the Basics section and work your way through. If you’re looking for a specific function or operator, jump straight to the Reference section. If you want to embed Bop in your own Rust project, see the Embedding chapter.

Syntax

Bop’s syntax is deliberately simple. If you’ve seen JavaScript or Python, most of it will look familiar.

Blocks

Code blocks use curly braces { } and only appear after control flow keywords (if, else, while, for, repeat, fn):

if count > 3 {
  print("That's a lot!")
}

Important: The opening { must be on the same line as its keyword. Bop automatically inserts semicolons at the end of lines, so putting { on the next line would cause a parse error.

// Good
if count > 3 {
  print("Nice!")
}

// Bad — will cause an error
if count > 3
{
  print("Nice!")
}

Statements

Statements end with a newline. Bop automatically inserts semicolons after lines ending in an identifier, literal, true, false, none, break, continue, return, ), ], or }.

You can put multiple statements on one line with an explicit semicolon:

let x = 1; let y = 2

Comments

Line comments start with //. Everything after // on that line is ignored:

// This is a comment
let x = 5   // So is this

Identifiers

Variable and function names start with a letter or underscore and can contain letters, digits, and underscores:

let my_var = 5
let _count = 0
let item3 = "hello"

Whitespace

Spaces and tabs are insignificant — use whatever indentation style you like. Only newlines matter (they end statements).

Types

Bop is dynamically typed — variables can hold any type, and types are checked at runtime. There are six types:

TypeLiteralsExamples
numberDigits, optional decimal0, 42, -7, 3.14
stringDouble-quoted"hello", "got {n} items"
boolKeywordstrue, false
noneKeywordnone
arraySquare brackets[1, 2, 3], []
dictCurly braces with colons{"x": 10, "name": "Alice"}

You can check a value’s type at runtime with the type() function:

let x = 42
print(type(x))    // "number"

let s = "hello"
print(type(s))    // "string"

Numbers

Bop has a single number type (64-bit floating point internally). Whole numbers display without a decimal point: 5 not 5.0.

let score = 100
let pi = 3.14
let negative = -7

Division always produces a float result:

print(7 / 2)     // 3.5
print(6 / 2)     // 3

Use int() to truncate the decimal part:

print(int(7 / 2))   // 3
print(int(3.9))      // 3

Strings

Strings use double quotes only. Supported escape sequences: \", \\, \n, \t, \{, \}.

let greeting = "Hello, world!"
let with_newline = "Line 1\nLine 2"

Strings are indexable and iterable, but immutable — you can read characters but not change them in place:

let s = "hello"
print(s[0])     // "h"
print(s[-1])    // "o"

for ch in s {
  print(ch)     // "h", "e", "l", "l", "o"
}

String interpolation

Use {variable} inside a string to insert a variable’s value. Only variable names are allowed inside {} — not expressions:

let name = "Alice"
let count = 5
print("Hello, {name}! You have {count} items.")

For computed values, store the result in a variable first:

let doubled = str(count * 2)
print("Double: {doubled}")

// Or use concatenation:
print("Double: " + str(count * 2))

To include a literal { or } in a string, escape it with a backslash:

print("Use \{name\} for interpolation")
// prints: Use {name} for interpolation

Booleans

true and false. Used in conditions and comparisons:

let found = true
let empty = false

if found {
  print("Got it!")
}

None

none represents the absence of a value. Functions that don’t explicitly return a value return none. It’s also what you get when looking up a missing dictionary key:

let stats = {"hp": 10}
let missing = stats["armor"]
print(missing)    // none

Variables

Variables store values that you can use and change throughout your program.

Declaring variables

Use let to create a new variable:

let x = 5
let name = "Alice"
let found = true
let items = [1, 2, 3]
let config = {"width": 10, "height": 5}

let is required the first time — using an undeclared variable is an error. This catches typos early:

let count = 5
conut = 10    // Error: I don't know what 'conut' is — did you mean 'count'?

Reassignment

After declaration, reassign with just =:

let score = 0
score = 10
score += 5     // score is now 15

Compound assignment operators work too: +=, -=, *=, /=, %=.

let x = 10
x += 3    // x = x + 3 → 13
x -= 1    // x = x - 1 → 12
x *= 2    // x = x * 2 → 24

Block scoping

Variables are block-scoped — a variable declared inside { } is not visible outside:

let x = 1
if true {
  let y = 2       // y only exists inside this block
  print(y)        // 2
}
// print(y)       // Error: I don't know what 'y' is

Shadowing

You can re-declare a variable with let in an inner block. The inner variable “shadows” the outer one:

let x = 1
if true {
  let x = 2     // shadows outer x
  x = 3         // reassigns inner x
  print(x)      // 3
}
print(x)         // 1 — outer x is unchanged

Copying and passing values

Every value in Bop is a copy. When you assign a variable, pass an argument to a function, or return a value, you get an independent copy — even for arrays and dicts. Changing the copy never affects the original.

Assignment copies

let a = [1, 2, 3]
let b = a           // b is a separate copy
b.push(4)
print(a)            // [1, 2, 3] — unchanged
print(b)            // [1, 2, 3, 4]

Function arguments are copies

When you pass a value to a function, the function gets its own copy. Modifying it inside the function has no effect on the caller’s variable:

fn try_to_modify(items) {
  items.push(99)
  print(items)       // [1, 2, 3, 99]
}

let original = [1, 2, 3]
try_to_modify(original)
print(original)      // [1, 2, 3] — unchanged

To get a modified value out of a function, return it:

fn add_item(items, val) {
  items.push(val)
  return items
}

let original = [1, 2, 3]
original = add_item(original, 99)
print(original)      // [1, 2, 3, 99]

This works the same way for all types — numbers, strings, bools, arrays, and dicts are all copied. There are no references or shared mutable state in Bop.

Dynamic typing

Variables can hold any type. You can even change the type of a variable by reassigning it:

let val = 42
print(type(val))    // "number"

val = "hello"
print(type(val))    // "string"

This flexibility is useful, but can be surprising if you’re not careful.

if / else

The if statement lets your program make decisions based on conditions.

Basic if

if count > 3 {
  print("That's a lot!")
}

The condition does not need parentheses, but they’re allowed: if (x > 3) { ... }. Braces are always required.

if / else

if temperature > 30 {
  print("It's hot!")
} else {
  print("Not too bad.")
}

if / else if / else

Chain multiple conditions with else if:

if score > 90 {
  print("Excellent!")
} else if score > 70 {
  print("Good job!")
} else if score > 50 {
  print("Not bad!")
} else {
  print("Keep trying!")
}

Only the first matching branch runs. If none match and there’s an else, that branch runs.

if as an expression

if/else can produce a value when used in expression position (e.g., after =):

let label = if count > 3 { "lots" } else { "few" }
print("You have {label} of items")

When used as an expression, both if and else branches are required. The last expression in each branch is the value.

let message = if x > 0 {
  "positive"
} else {
  "non-positive"
}
print(message)

Common patterns

Guard clause

fn process(value) {
  if value == none {
    return
  }
  print("Processing: " + str(value))
}

Classify a value

fn classify(n) {
  if n > 0 {
    return "positive"
  } else if n < 0 {
    return "negative"
  } else {
    return "zero"
  }
}

Combine conditions with && and ||

if age >= 18 && has_ticket {
  print("Welcome in!")
}

if x < 0 || x > 100 {
  print("Out of range!")
}

Loops

Loops let you repeat actions. Bop has three kinds: repeat, while, and for...in.

repeat

The simplest loop — just “do this N times.” No loop variable, no fuss:

repeat 4 {
  print("Hello!")
}

The count can be any expression:

let times = 3
repeat times {
  print("Again!")
}

repeat is perfect when you just need repetition without tracking a counter.

while

Loops as long as a condition is true:

let n = 1
while n <= 100 {
  n *= 2
}
print(n)    // 128

A while true loop runs forever (until you break out of it or hit the step limit):

let total = 0
let i = 1
while true {
  total += i
  if total > 100 {
    break
  }
  i += 1
}
print("Sum exceeded 100 at i=" + str(i))

Counting example

// Count how many numbers under 50 are divisible by 7
let count = 0
let n = 1
while n < 50 {
  if n % 7 == 0 {
    count += 1
  }
  n += 1
}
print("Found " + str(count))

for…in

Iterates over ranges, arrays, or dictionary keys.

Ranges

for i in range(5) {
  print(str(i))     // 0, 1, 2, 3, 4
}

With a start value:

for i in range(2, 8) {
  print(str(i))     // 2, 3, 4, 5, 6, 7
}

Arrays

let fruits = ["apple", "banana", "cherry"]
for fruit in fruits {
  print(fruit)
}

Dictionary keys

let scores = {"Alice": 95, "Bob": 87, "Charlie": 92}
for name in scores {
  let s = str(scores[name])
  print(name + ": " + s)
}

Strings

You can iterate over the characters of a string:

let word = "hello"
for ch in word {
  print(ch)    // "h", "e", "l", "l", "o"
}

Nesting loops

Loops can be nested. This is useful for working with grids or combinations:

for row in range(3) {
  for col in range(4) {
    print("(" + str(row) + ", " + str(col) + ")")
  }
}

break & continue

break and continue give you finer control over loops.

break

Exits the loop immediately. Execution continues after the loop:

let i = 0
while true {
  if i >= 10 {
    break
  }
  i += 1
}
print("Stopped at " + str(i))

Searching for something

let numbers = [4, 8, 15, 16, 23, 42]
let found = false
for n in numbers {
  if n > 20 {
    found = true
    break
  }
}

if found {
  print("Found a number greater than 20!")
} else {
  print("No number greater than 20.")
}

continue

Skips the rest of the current iteration and jumps to the next one:

for i in range(10) {
  if i % 2 == 0 {
    continue
  }
  print(str(i))    // 1, 3, 5, 7, 9
}

Filter and process

let words = ["hello", "", "world", "", "bop"]
for word in words {
  if word == "" {
    continue
  }
  print(word.upper())
}

Which loops support break and continue?

All three loop types — while, for...in, and repeat — support both break and continue.

repeat 10 {
  let n = rand(100)
  if n < 10 {
    break
  }
  print(str(n))
}

Note: break and continue only affect the innermost loop. If you have nested loops and want to exit the outer one, use a variable flag or a function with return.

Arrays

Arrays are ordered, mutable collections that can hold any mix of types.

Creating arrays

let items = [1, 2, 3]
let empty = []
let mixed = [1, "two", true, none]

Accessing elements

Arrays are 0-indexed. Negative indices count from the end:

let items = [10, 20, 30]
print(items[0])     // 10
print(items[2])     // 30
print(items[-1])    // 30 (last element)
print(items[-2])    // 20

Out-of-bounds access produces an error.

Modifying elements

let items = [10, 20, 30]
items[0] = 99
print(items)    // [99, 20, 30]

Methods

MethodReturnsDescription
arr.len()numberNumber of elements
arr.push(val)noneAppend to end
arr.pop()valueRemove and return last element
arr.has(val)boolWhether the array contains the value
arr.index_of(val)number or noneIndex of first occurrence, or none
arr.insert(i, val)noneInsert at index, shifting elements right
arr.remove(i)valueRemove at index, shifting elements left
arr.slice(start, end)arrayNew sub-array (both args optional)
arr.reverse()arrayReverse in place, returns the array
arr.sort()arraySort in place, returns the array
arr.join(sep)stringJoin elements into a string

Practical examples

Building a list

let squares = []
for i in range(1, 6) {
  squares.push(i * i)
}
print(squares)    // [1, 4, 9, 16, 25]

Filtering values

let numbers = [3, 7, 1, 9, 4, 6, 2, 8]
let big = []
for n in numbers {
  if n > 5 {
    big.push(n)
  }
}
print(big)    // [7, 9, 6, 8]

Checking membership

let allowed = ["admin", "editor", "viewer"]
let role = "editor"
if allowed.has(role) {
  print("Access granted")
} else {
  print("Access denied")
}

Sorting and joining

let scores = [42, 17, 85, 3]
scores.sort()
print(scores)    // [3, 17, 42, 85]

let names = ["Charlie", "Alice", "Bob"]
names.sort()
print(names.join(", "))    // "Alice, Bob, Charlie"

Strings

Strings are immutable sequences of characters. All string methods return new strings — the original is never modified.

Creating strings

let s = "hello world"
let empty = ""
let escaped = "Line 1\nLine 2"

Supported escape sequences: \", \\, \n, \t, \{, \}.

Indexing

let s = "hello"
print(s[0])      // "h"
print(s[-1])     // "o"

Each index returns a single-character string (there’s no separate character type).

String interpolation

Insert variable values with {name} inside a string:

let name = "Alice"
let count = 5
print("Hello, {name}! You have {count} items.")

Only variable names are allowed inside {}. For expressions, use a temporary variable:

let total = str(count * 2)
print("Double: {total}")

Or use concatenation:

print("Double: " + str(count * 2))

To include a literal { or } in a string, escape it with \{ and \}:

print("Use \{name\} for interpolation")
// prints: Use {name} for interpolation

Concatenation

Use + to join strings:

let full = "Hello" + ", " + "world!"
print(full)    // "Hello, world!"

Numbers must be converted with str() first:

let msg = "Score: " + str(42)

Methods

MethodReturnsDescription
s.len()numberNumber of characters
s.contains(sub)boolWhether the string contains sub
s.starts_with(prefix)boolWhether it starts with prefix
s.ends_with(suffix)boolWhether it ends with suffix
s.index_of(sub)number or noneIndex of first occurrence, or none
s.split(sep)arraySplit into array of strings on sep
s.replace(old, new)stringReplace all occurrences
s.upper()stringUppercase copy
s.lower()stringLowercase copy
s.trim()stringCopy with leading/trailing whitespace removed
s.slice(start, end)stringSubstring (both args optional)

Practical examples

Parsing CSV data

let input = "Alice,95,A"
let parts = input.split(",")
print(parts[0])    // "Alice"
print(parts[1])    // "95"

Checking prefixes

let filename = "report.csv"
if filename.ends_with(".csv") {
  print("CSV file detected")
}

Building a formatted string

let items = ["apple", "banana", "cherry"]
let count = str(items.len())
let list = items.join(", ")
print("Found {count} items: {list}")

Dictionaries

Dictionaries (dicts) are key-value stores. Keys are always strings; values can be any type.

Creating dictionaries

let person = {"name": "Alice", "age": 30, "active": true}
let empty = {}

Accessing values

Use bracket notation with a string key:

let name = person["name"]     // "Alice"
let age = person["age"]       // 30

Accessing a missing key returns none (no error):

let email = person["email"]
print(email)    // none

Modifying values

person["age"] = 31             // update existing key
person["email"] = "a@b.com"   // add new entry

Methods

MethodReturnsDescription
d.len()numberNumber of entries
d.keys()arrayArray of all keys
d.values()arrayArray of all values
d.has(key)boolWhether the key exists

Practical examples

Counting occurrences

let words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
let counts = {}
for word in words {
  if counts.has(word) {
    counts[word] += 1
  } else {
    counts[word] = 1
  }
}

for key in counts {
  print(key + ": " + str(counts[key]))
}

Storing structured data

let point = {"x": 10, "y": 20}
let x = str(point["x"])
let y = str(point["y"])
print("Position: ({x}, {y})")

Iterating over entries

let config = {"width": 800, "height": 600, "title": "My App"}
for key in config {
  let val = str(config[key])
  print(key + ": " + val)
}

Checking for a key before using it

let settings = {"volume": 80}

if settings.has("volume") {
  let v = str(settings["volume"])
  print("Volume is {v}")
} else {
  print("Using default volume")
}

Defining Functions

Functions let you name a sequence of actions and reuse it. This is where Bop starts to feel like real programming.

Declaring a function

fn greet() {
  print("Hello!")
}

greet()    // "Hello!"
greet()    // "Hello!"

Parameters

Functions can take parameters — values you pass in when calling:

fn repeat_string(text, times) {
  let result = ""
  repeat times {
    result += text
  }
  return result
}

print(repeat_string("ha", 3))    // "hahaha"

Parameters are positional. There are no default values or type annotations.

Return values

Use return to send a value back from the function:

fn double(x) {
  return x * 2
}

let result = double(5)    // 10
print(str(result))

return with no value (or reaching the end of the function) returns none:

fn do_something() {
  print("Working...")
  // no return — returns none
}

let result = do_something()
print(result)    // none

Early return

return exits the function immediately, even from inside loops or conditionals:

fn find_first_big(numbers, threshold) {
  for n in numbers {
    if n > threshold {
      return n
    }
  }
  return none
}

let result = find_first_big([3, 7, 1, 15, 4], 10)
print(str(result))    // 15

Calling functions

Parentheses are always required, even with no arguments:

greet()          // correct
// greet         // error — 'greet' is a function, call it with greet()

Practical example: sum of squares

fn sum_of_squares(n) {
  let total = 0
  for i in range(1, n + 1) {
    total += i * i
  }
  return total
}

let result = sum_of_squares(5)
print("Sum of squares: " + str(result))    // 55

Recursion

Functions can call themselves. Recursion is capped by the step limit to prevent infinite recursion:

fn factorial(n) {
  if n <= 1 {
    return 1
  }
  return n * factorial(n - 1)
}

print(str(factorial(5)))    // 120

Note: Functions are named declarations only. You can’t assign a function to a variable or pass one as an argument (yet). First-class functions may be added in a future version.

Operators

All Bop operators, grouped by category. No operator overloading — each operator works on specific types and produces an error on type mismatch.

Arithmetic

OperatorNameTypesNotes
+AddnumberAlso concatenates strings: "a" + "b""ab"
-SubtractnumberAlso unary negation: -x
*Multiplynumber
/DividenumberAlways produces float: 7 / 23.5
%Modulonumber
print(10 + 3)      // 13
print(10 - 3)      // 7
print(10 * 3)      // 30
print(10 / 3)      // 3.333...
print(10 % 3)      // 1

print("Hello" + " " + "world")    // "Hello world"

Comparison

OperatorNameReturns
==Equalbool
!=Not equalbool
<Less thanbool
>Greater thanbool
<=Less or equalbool
>=Greater or equalbool

== and != work on all types. Comparing different types is always false (no implicit coercion). <, >, <=, >= work on numbers and strings (lexicographic).

print(5 == 5)         // true
print(5 == "5")       // false (different types)
print("abc" < "def")  // true (lexicographic)

Boolean

OperatorNameNotes
&&AndShort-circuits
||OrShort-circuits
!NotUnary prefix

Short-circuiting means the second operand isn’t evaluated if the first determines the result:

// && stops at the first false
if x > 0 && x < 100 {
  print("In range")
}

// || stops at the first true
if name == "" || name == none {
  print("No name provided")
}

// ! inverts a boolean
if !found {
  print("Still searching...")
}

Assignment

OperatorEquivalent to
=Assign
+=x = x + ...
-=x = x - ...
*=x = x * ...
/=x = x / ...
%=x = x % ...
let score = 0
score += 10    // 10
score -= 3     // 7
score *= 2     // 14

Conditional expressions

Bop has no ternary operator (?:). Use if/else as an expression instead:

let label = if count > 3 { "lots" } else { "few" }

Both branches are required when if/else is used as an expression. The last expression in each branch is the value.

Precedence

From highest (evaluated first) to lowest:

PriorityOperators
1!, - (unary)
2*, /, %
3+, -
4<, >, <=, >=
5==, !=
6&&
7||
8=, +=, -=, *=, /=, %=

Parentheses override precedence:

let result = (1 + 2) * 3    // 9, not 7

Built-in Functions

Bop provides 11 built-in functions available in every program. These cannot be shadowed by user-defined functions.

Output

print(args...)

Prints values to output, separated by spaces. Returns none.

print("hello")           // hello
print("x =", 42)         // x = 42
print(1, "plus", 2)      // 1 plus 2

print accepts any number of arguments (including zero). Each argument is converted to its string representation automatically.

inspect(value)

Returns a debug representation of a value as a string. Strings are wrapped in quotes; other types look the same as str().

print(inspect("hello"))    // "hello"
print(inspect(42))         // 42
print(inspect([1, 2]))     // [1, 2]

Useful for debugging when you need to distinguish strings from other types.

Type Conversion

str(value)

Converts any value to its string representation.

str(42)        // "42"
str(3.14)      // "3.14"
str(true)      // "true"
str(none)      // "none"
str([1, 2])    // "[1, 2]"

int(value)

Truncates a number to an integer, or parses a string as a number and truncates.

int(3.9)       // 3
int(-2.7)      // -2
int("42")      // 42
int(true)      // 1
int(false)     // 0

Produces an error if the value can’t be converted (e.g., int("hello")).

type(value)

Returns the type name of a value as a string.

type(42)           // "number"
type("hello")      // "string"
type(true)         // "bool"
type(none)         // "none"
type([1, 2])       // "array"
type({"a": 1})     // "dict"

Math

abs(x)

Returns the absolute value of a number.

abs(-5)     // 5
abs(3)      // 3
abs(-2.7)   // 2.7

min(a, b)

Returns the smaller of two numbers.

min(3, 7)      // 3
min(-1, -5)    // -5

max(a, b)

Returns the larger of two numbers.

max(3, 7)      // 7
max(-1, -5)    // -1

rand(n)

Returns a random integer from 0 to n-1 (inclusive). The argument must be a positive integer.

rand(6)     // 0, 1, 2, 3, 4, or 5
rand(2)     // 0 or 1 (coin flip)
rand(100)   // 0 to 99

Note: rand uses a deterministic pseudo-random number generator. The same seed produces the same sequence of values.

Collections

len(value)

Returns the length of a string, array, or dictionary.

len("hello")       // 5
len([1, 2, 3])     // 3
len({"a": 1})      // 1
len("")            // 0
len([])            // 0

Produces an error for numbers, bools, and none.

Ranges

range(n) / range(start, end) / range(start, end, step)

Generates an array of numbers.

range(5)           // [0, 1, 2, 3, 4]
range(2, 6)        // [2, 3, 4, 5]
range(0, 10, 2)    // [0, 2, 4, 6, 8]
range(5, 0)        // [5, 4, 3, 2, 1]  (auto-detects direction)
range(10, 0, -3)   // [10, 7, 4, 1]
  • With 1 argument: range(n) produces [0, 1, ..., n-1]
  • With 2 arguments: range(start, end) auto-detects direction
  • With 3 arguments: range(start, end, step) uses the given step (error if step is 0)
  • Maximum 10,000 elements

Grammar

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

Reserved words

let fn return if else while for in repeat break continue
true false none

These cannot be used as variable or function names.

Built-in functions

Built-in functions available everywhere:

FunctionReturnsDescription
range(n)array[0, 1, ..., n-1]
range(start, end)array[start, start+1, ..., end-1] (auto-detects direction)
range(start, end, step)arraySequence with custom step
str(x)stringConvert any value to string
int(x)numberTruncate to integer, or parse string
type(x)stringType name: "number", "string", "bool", "none", "array", "dict"
abs(x)numberAbsolute value
min(a, b)numberSmaller of two numbers
max(a, b)numberLarger of two numbers
rand(n)numberRandom integer 0 to n-1
len(x)numberLength of string, array, or dict
print(args...)nonePrint values separated by spaces
inspect(x)stringDebug representation (strings quoted)

Grammar

program     = statement*
statement   = letDecl | assignment | ifStmt | whileStmt | repeatStmt
            | forStmt | fnDecl | returnStmt | breakStmt | continueStmt
            | exprStmt

letDecl     = "let" IDENT "=" expr
assignment  = target ("=" | "+=" | "-=" | "*=" | "/=" | "%=") expr
target      = IDENT | expr "[" expr "]"

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

exprStmt    = expr

expr        = or
or          = and ("||" and)*
and         = equality ("&&" equality)*
equality    = comparison (("==" | "!=") comparison)*
comparison  = addition (("<" | ">" | "<=" | ">=") addition)*
addition    = multiply (("+" | "-") multiply)*
multiply    = unary (("*" | "/" | "%") unary)*
unary       = ("!" | "-") unary | postfix
postfix     = primary (call | index | method)*
call        = "(" args? ")"
index       = "[" expr "]"
method      = "." IDENT "(" args? ")"
primary     = NUMBER | STRING | "true" | "false" | "none"
            | IDENT | "(" expr ")" | arrayLit | dictLit
            | ifExpr

arrayLit    = "[" (expr ("," expr)*)? "]"
dictLit     = "{" (STRING ":" expr ("," STRING ":" expr)*)? "}"
ifExpr      = "if" expr "{" expr "}" "else" "{" expr "}"

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

Automatic semicolons

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

  • An identifier or literal
  • true, false, none
  • break, continue, return
  • ), ], }

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")
}

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.

Embedding Bop

Bop is designed to be embedded in Rust applications. You can add custom functions, capture output, and control execution through the BopHost trait.

Quick start

Add bop-lang to your Cargo.toml:

[dependencies]
bop = { package = "bop-lang", version = "0.1" }

Run a program with the default host:

use bop::{run, StdHost, BopLimits};

fn main() {
    let source = r#"
        let name = "world"
        print("Hello, {name}!")
    "#;

    let mut host = StdHost;
    let limits = BopLimits::standard();

    if let Err(e) = run(source, &mut host, &limits) {
        eprintln!("Error: {e}");
    }
}

The BopHost trait

The BopHost trait is the integration point between your application and the Bop interpreter:

#![allow(unused)]
fn main() {
pub trait BopHost {
    /// Called for unknown function names. Return `None` if not handled.
    fn call(
        &mut self,
        name: &str,
        args: &[Value],
        line: u32,
    ) -> Option<Result<Value, BopError>>;

    /// Called by `print()`. Default: writes to stdout.
    fn on_print(&mut self, message: &str) {
        println!("{}", message);
    }

    /// Hint text appended to "function not found" errors.
    fn function_hint(&self) -> &str {
        ""
    }

    /// Called each tick (statement, loop iteration). Return Err to halt.
    fn on_tick(&mut self) -> Result<(), BopError> {
        Ok(())
    }
}
}

call — Custom functions

This is the primary extension point. When Bop encounters a function call that isn’t a built-in or user-defined function, it calls host.call(). Return None if you don’t handle the function name — Bop will report a “function not found” error. Return Some(Ok(value)) on success, or Some(Err(error)) to raise a runtime error.

#![allow(unused)]
fn main() {
use bop::{BopHost, BopError, Value};

struct MyHost;

impl BopHost for MyHost {
    fn call(
        &mut self,
        name: &str,
        args: &[Value],
        line: u32,
    ) -> Option<Result<Value, BopError>> {
        match name {
            "square" => {
                if args.len() != 1 {
                    return Some(Err(BopError::runtime(
                        "square() takes 1 argument", line
                    )));
                }
                match &args[0] {
                    Value::Number(n) => Some(Ok(Value::Number(n * n))),
                    _ => Some(Err(BopError::runtime(
                        "square() requires a number", line
                    ))),
                }
            }
            _ => None, // not handled
        }
    }
}
}

Bop scripts can then call square() as if it were built-in:

let result = square(5)
print(str(result))    // 25

on_print — Capturing output

Override on_print to redirect print() output to a buffer, log, UI widget, or anywhere else:

#![allow(unused)]
fn main() {
struct BufferedHost {
    output: Vec<String>,
}

impl BopHost for BufferedHost {
    fn call(&mut self, _: &str, _: &[Value], _: u32)
        -> Option<Result<Value, BopError>>
    {
        None
    }

    fn on_print(&mut self, message: &str) {
        self.output.push(message.to_string());
    }
}
}

on_tick — Execution control

on_tick is called on every interpreter step (each statement, loop iteration, etc.). Use it for:

  • Timeouts — check elapsed time and halt if exceeded
  • Cancellation — check a flag set by another thread
  • Progress tracking — count steps or update a progress bar
#![allow(unused)]
fn main() {
use std::time::{Duration, Instant};

struct TimedHost {
    start: Instant,
    timeout: Duration,
}

impl BopHost for TimedHost {
    fn call(&mut self, _: &str, _: &[Value], _: u32)
        -> Option<Result<Value, BopError>>
    {
        None
    }

    fn on_tick(&mut self) -> Result<(), BopError> {
        if self.start.elapsed() > self.timeout {
            Err(BopError::runtime("execution timed out", 0))
        } else {
            Ok(())
        }
    }
}
}

function_hint — Better error messages

Return a hint string that gets appended to “function not found” errors. This helps guide users toward the functions your host provides:

#![allow(unused)]
fn main() {
fn function_hint(&self) -> &str {
    "Available functions: square(), cube(), sqrt()"
}
}

Resource limits

BopLimits controls how much work a script can do:

#![allow(unused)]
fn main() {
pub struct BopLimits {
    pub max_steps: u64,      // loop iterations, statements, etc.
    pub max_memory: usize,   // bytes for strings + arrays
}
}

Two presets are provided:

Presetmax_stepsmax_memory
BopLimits::standard()10,00010 MB
BopLimits::demo()1,0001 MB

Or create your own:

#![allow(unused)]
fn main() {
let limits = BopLimits {
    max_steps: 50_000,
    max_memory: 32 * 1024 * 1024, // 32 MB
};
}

When a limit is exceeded, run() returns a BopError — the script is halted cleanly without panicking.

Putting it all together

Here’s a complete example of a host that provides domain-specific functions, captures output, and enforces a timeout:

use bop::{run, BopHost, BopError, BopLimits, Value};
use std::time::{Duration, Instant};

struct AppHost {
    output: Vec<String>,
    start: Instant,
    data: Vec<f64>,
}

impl BopHost for AppHost {
    fn call(
        &mut self,
        name: &str,
        args: &[Value],
        line: u32,
    ) -> Option<Result<Value, BopError>> {
        match name {
            "add_data" => {
                if let Some(Value::Number(n)) = args.first() {
                    self.data.push(*n);
                    Some(Ok(Value::None))
                } else {
                    Some(Err(BopError::runtime(
                        "add_data() requires a number", line
                    )))
                }
            }
            "average" => {
                if self.data.is_empty() {
                    Some(Ok(Value::Number(0.0)))
                } else {
                    let sum: f64 = self.data.iter().sum();
                    Some(Ok(Value::Number(sum / self.data.len() as f64)))
                }
            }
            _ => None,
        }
    }

    fn on_print(&mut self, message: &str) {
        self.output.push(message.to_string());
    }

    fn on_tick(&mut self) -> Result<(), BopError> {
        if self.start.elapsed() > Duration::from_secs(5) {
            Err(BopError::runtime("timed out", 0))
        } else {
            Ok(())
        }
    }

    fn function_hint(&self) -> &str {
        "Available: add_data(n), average()"
    }
}

fn main() {
    let source = r#"
        for n in [10, 20, 30, 40, 50] {
            add_data(n)
        }
        print("Average: " + str(average()))
    "#;

    let mut host = AppHost {
        output: vec![],
        start: Instant::now(),
        data: vec![],
    };

    match run(source, &mut host, &BopLimits::standard()) {
        Ok(()) => {
            for line in &host.output {
                println!("{line}");
            }
        }
        Err(e) => eprintln!("Script error: {e}"),
    }
}