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() returns | Description | Literal examples |
|---|---|---|
"int" | 64-bit signed integer | 0, 42, -7 |
"number" | 64-bit floating point | 3.14, -0.5, 4.0 |
"string" | UTF-8 text | "hello", "got {n} items" |
"bool" | Boolean | true, false |
"none" | Absence of a value | none |
"array" | Ordered, mutable collection | [1, 2, 3], [] |
"dict" | String-keyed map | {"x": 10, "name": "Alice"} |
"fn" | First-class function / closure | fn(x) { return x + 1 } |
"struct" | User-defined struct instance | Point { x: 3, y: 4 } |
"enum" | User-defined enum variant | Color::Red |
"module" | Aliased module namespace | result 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.