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
BopHosttrait 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:
| Type | Literals | Examples |
|---|---|---|
| number | Digits, optional decimal | 0, 42, -7, 3.14 |
| string | Double-quoted | "hello", "got {n} items" |
| bool | Keywords | true, false |
| none | Keyword | none |
| array | Square brackets | [1, 2, 3], [] |
| dict | Curly 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:
breakandcontinueonly affect the innermost loop. If you have nested loops and want to exit the outer one, use a variable flag or a function withreturn.
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
| Method | Returns | Description |
|---|---|---|
arr.len() | number | Number of elements |
arr.push(val) | none | Append to end |
arr.pop() | value | Remove and return last element |
arr.has(val) | bool | Whether the array contains the value |
arr.index_of(val) | number or none | Index of first occurrence, or none |
arr.insert(i, val) | none | Insert at index, shifting elements right |
arr.remove(i) | value | Remove at index, shifting elements left |
arr.slice(start, end) | array | New sub-array (both args optional) |
arr.reverse() | array | Reverse in place, returns the array |
arr.sort() | array | Sort in place, returns the array |
arr.join(sep) | string | Join 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
| Method | Returns | Description |
|---|---|---|
s.len() | number | Number of characters |
s.contains(sub) | bool | Whether the string contains sub |
s.starts_with(prefix) | bool | Whether it starts with prefix |
s.ends_with(suffix) | bool | Whether it ends with suffix |
s.index_of(sub) | number or none | Index of first occurrence, or none |
s.split(sep) | array | Split into array of strings on sep |
s.replace(old, new) | string | Replace all occurrences |
s.upper() | string | Uppercase copy |
s.lower() | string | Lowercase copy |
s.trim() | string | Copy with leading/trailing whitespace removed |
s.slice(start, end) | string | Substring (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
| Method | Returns | Description |
|---|---|---|
d.len() | number | Number of entries |
d.keys() | array | Array of all keys |
d.values() | array | Array of all values |
d.has(key) | bool | Whether 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
| Operator | Name | Types | Notes |
|---|---|---|---|
+ | Add | number | Also concatenates strings: "a" + "b" → "ab" |
- | Subtract | number | Also unary negation: -x |
* | Multiply | number | |
/ | Divide | number | Always produces float: 7 / 2 → 3.5 |
% | Modulo | number |
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
| Operator | Name | Returns |
|---|---|---|
== | Equal | bool |
!= | Not equal | bool |
< | Less than | bool |
> | Greater than | bool |
<= | Less or equal | bool |
>= | Greater or equal | bool |
== 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
| Operator | Name | Notes |
|---|---|---|
&& | And | Short-circuits |
|| | Or | Short-circuits |
! | Not | Unary 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
| Operator | Equivalent 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:
| Priority | Operators |
|---|---|
| 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:
randuses 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:
| Function | Returns | Description |
|---|---|---|
range(n) | array | [0, 1, ..., n-1] |
range(start, end) | array | [start, start+1, ..., end-1] (auto-detects direction) |
range(start, end, step) | array | Sequence with custom step |
str(x) | string | Convert any value to string |
int(x) | number | Truncate to integer, or parse string |
type(x) | string | Type name: "number", "string", "bool", "none", "array", "dict" |
abs(x) | number | Absolute value |
min(a, b) | number | Smaller of two numbers |
max(a, b) | number | Larger of two numbers |
rand(n) | number | Random integer 0 to n-1 |
len(x) | number | Length of string, array, or dict |
print(args...) | none | Print values separated by spaces |
inspect(x) | string | Debug 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,nonebreak,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:
| Preset | max_steps | max_memory |
|---|---|---|
BopLimits::standard() | 10,000 | 10 MB |
BopLimits::demo() | 1,000 | 1 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}"),
}
}