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

Types

Bop is dynamically typed — variables can hold any type, and types are checked at runtime. Every value has a .type() method that returns its type name:

.type() returnsDescriptionLiteral examples
"int"64-bit signed integer0, 42, -7
"number"64-bit floating point3.14, -0.5, 4.0
"string"UTF-8 text"hello", "got {n} items"
"bool"Booleantrue, false
"none"Absence of a valuenone
"array"Ordered, mutable collection[1, 2, 3], []
"dict"String-keyed map{"x": 10, "name": "Alice"}
"fn"First-class function / closurefn(x) { return x + 1 }
"struct"User-defined struct instancePoint { x: 3, y: 4 }
"enum"User-defined enum variantColor::Red
"module"Aliased module namespaceresult of use foo as m
"iter"Lazy iterator[1, 2, 3].iter(), "abc".iter()
let x = 42
print(x.type())      // "int"

let y = 3.14
print(y.type())      // "number"

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

Integers and floats

Integer literals (42, -7) produce int values; anything with a decimal point (3.14, 4.0) produces number. There’s no exponent-shaped literal — write the full decimal or build large values by multiplication. The two numeric types coexist: arithmetic widens to number on mixed operands, and == compares numerically across the split:

print(1 + 1)         // 2       (int + int → int)
print(1 + 1.0)       // 2       (int + number → number, prints as whole)
print(1 == 1.0)      // true    (cross-type numeric equality)

Use .to_int() to truncate a number to an integer (toward zero), and .to_float() to widen an int to a number:

print((3.7).to_int())      // 3
print((-2.7).to_int())     // -2
print((5).to_float())      // 5       (now a number internally)

Number literals need parens before a method call (otherwise 3.7.to_int() looks like a decimal followed by a field). Variables don’t: let x = 3.7; print(x.to_int()).

Division

/ always produces a number, even for int / int:

print(7 / 2)              // 3.5
print(6 / 2)              // 3        (whole value — still a number)
print((6 / 2).type())     // "number"

This sidesteps the classic “1 / 2 == 0” footgun that trips beginners in C / Rust / Java.

When you do want an integer result (index math, bucketing, etc.), coerce the quotient back with .to_int():

print((7 / 2).to_int())           // 3
print((-7 / 2).to_int())          // -3
print((7 / 2).to_int().type())    // "int"

.to_int() truncates toward zero. / raises a runtime error on division by zero.

Strings

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

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 = count * 2
print("Double: {doubled}")

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

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

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

String concatenation

+ joins two strings, or a string and a number (the number is converted first):

print("Score: " + (42).to_str())    // "Score: 42"
print("n=" + 7)                      // "n=7"   (int auto-stringified)

Booleans

true and false. Used in conditions and comparisons:

let found = true

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 from any operation designed to signal “no value here” (e.g. first_or_none helpers, optional return values):

fn first_or_none(arr) {
  if arr.len() == 0 { return none }
  return arr[0]
}

let r = first_or_none([])
print(r)           // none

Checking for none

Two equivalent ways:

if r.is_none() { print("empty") }
if r == none    { print("empty") }      // same thing

if r.is_some() { print("got one") }     // inverse — every non-none value is "some"

.is_none() / .is_some() are universal methods — they work on any value, not just optional-shaped ones. In a dynamically-typed language every variable can hold none, so the check is always available.

Don’t confuse falsy-ness with none-ness: false, 0, "", [], and {} are all falsy in if conditions but they’re not none. none.is_none() is true; false.is_none() is false.

User-defined types

Bop also lets you declare your own struct and enum types with methods — see Structs & Enums.