Documentation
Bop language guide

Methods

Methods🔗

Bop dispatches methods with .name(args...). Every built-in method on primitives, arrays, strings, and dicts is listed here. User-defined methods on structs use the same syntax — see Structs & Enums for the fn Type.method(self, ...) form.

Methods on every value🔗

Three methods work on any value — introspection + stringification. They’re dispatched before the type-specific tables, so they’re always available:

MethodReturnsNotes
x.type()stringOne of "int", "number", "string", "bool", "none", "array", "dict", "fn", "struct", "enum", "module", "iter"
x.to_str()stringDisplay repr — same as what print(x) would emit for a single arg
x.inspect()stringDebug repr — strings are wrapped in "...", nested strings stay quoted inside arrays / dicts
x.is_none()booltrue iff x is the none value. Equivalent to x == none.
x.is_some()boolInverse of .is_none()true for every value except none.
print((42).type())                 // "int"
print("hi".to_str())               // "hi"
print("hi".inspect())              // "hi"   (quoted)
print([1, "two"].inspect())        // [1, "two"]

print(none.is_none())              // true
print((0).is_none())               // false — `0` is falsy but not `none`
print(first_result().is_some())    // check an optional return without `== none`

.is_none() / .is_some() cover Bop’s “any variable can be none” story — they work on every receiver, not just Option-shaped ones (Bop doesn’t have Option). Equivalent to x == none / x != none, but reads better in method chains.

Parens around numeric literals🔗

Number literals need parens before a method call because . is otherwise a decimal point:

// print(42.type())   // parse error — `42.t…` looks like a decimal
print((42).type())     // "int"
print((-5).abs())      // 5

Identifiers don’t have this problem: x.type(), count.to_str(), etc.

Numeric methods — int and number🔗

All of these work on both int and number receivers. Return type is noted per method; most math operations always widen to number.

MethodReturnsDescription
x.abs()int / numberAbsolute value. Preserves receiver type. (-5).abs()5 (int), (-2.7).abs()2.7 (number). Integer overflow on (i64::MIN).abs() is a runtime error.
x.sqrt()numberSquare root.
x.sin(), x.cos(), x.tan()numberTrig. Angles in radians.
x.exp()numbere^x.
x.log()numberNatural log.
x.pow(e)numberx raised to e.
x.floor(), x.ceil(), x.round()int / numberRound toward -∞, +∞, or nearest (ties away from zero). Returns int when the rounded result fits in i64, number otherwise (so rounding a number that overflows i64 stays a number instead of raising). Int receivers pass through unchanged.
a.min(b), a.max(b)int / numberPair-wise. Preserves type when both sides match; widens to number on mixed int/number.
x.to_int()intTruncates toward zero. (3.7).to_int()3, (-2.7).to_int()-2.
x.to_float()numberWidens intnumber; number passes through.
print((9).sqrt())                   // 3
print((0).cos())                    // 1
print((2).pow(10))                  // 1024
print((3).min(7))                   // 3
print((1).max(2.5))                 // 2.5   (widened)
print((3.7).floor())                // 3     (int)
print((3.7).floor().type())         // "int"

Boolean methods — bool🔗

MethodReturnsDescription
b.to_int()inttrue.to_int()1, false.to_int()0.
b.to_float()numbertrue.to_float()1 (as number), false.to_float()0.

Plus the universal type / to_str / inspect.

String methods — string🔗

See Strings for worked examples.

MethodReturnsDescription
s.len()intNumber of Unicode code points.
s.contains(sub)boolWhether sub appears anywhere.
s.starts_with(prefix)bool
s.ends_with(suffix)bool
s.index_of(sub)intByte index of first occurrence, or -1 if not found.
s.split(sep)arraySplit into an array of strings on sep.
s.replace(old, new)stringReplace every occurrence.
s.upper(), s.lower()stringCase conversion.
s.trim()stringStrip leading / trailing whitespace.
s.slice(start, end)stringHalf-open substring by code-point index. Negative bounds count from the end; out-of-range bounds clamp.
s.to_int()intParse. "3.7".to_int() parses as float then truncates → 3. Raises on junk.
s.to_float()numberParse. Raises on junk.
s.iter()iterLazy iterator over Unicode code points. See Iter methods.

Array methods — array🔗

See Arrays for worked examples.

Mutating methods write back when the receiver is a variable (items.push(x)). Calling one on a genuine temporary, such as [1].push(2) or make_items().pop(), is legal; the temporary is mutated and then discarded. An index or field read is not yet a write-back place, so dict["items"].push(x) and holder.items.pop() raise a runtime error instead of silently losing the mutation. Assign the nested array to a variable first, then explicitly store it back when needed.

MethodReturnsDescription
arr.len()intNumber of elements.
arr.push(v)noneAppend.
arr.pop()valueRemove and return the last element.
arr.has(v)boolStructural equality check.
arr.index_of(v)intIndex of first match, or -1.
arr.insert(i, v)noneInsert at a signed index, shifting right. Negative indices count from the end; len appends.
arr.remove(i)valueRemove at a signed index, returning the removed value. Negative indices count from the end.
arr.slice(start, end)arrayHalf-open sub-array. Negative bounds count from the end; out-of-range bounds clamp.
arr.reverse()noneIn-place.
arr.sort()noneIn-place, numeric or lexicographic depending on element types.
arr.join(sep)stringJoin after stringifying each element.
arr.iter()iterLazy iterator over the elements. See Iter methods.

Dict methods — dict🔗

See Dictionaries for worked examples.

MethodReturnsDescription
d.len()intNumber of entries.
d.keys()arrayAll keys as strings.
d.values()arrayAll values.
d.has(key)boolWhether key exists.
d.iter()iterLazy iterator over keys, in declaration order. See Iter methods.

Result methods — Result🔗

Result is an engine built-in. All combinators are methods on the built-in type — no import required. Ok(x) and Err(e) are parser-level shorthand for Result::Ok(x) / Result::Err(e) in both expression and pattern position.

MethodReturnsDescription
r.is_ok()booltrue when r is Result::Ok(_).
r.is_err()booltrue when r is Result::Err(_).
r.unwrap()valuePayload on Ok; raises a runtime error on Err (message includes the .inspect() of the payload).
r.expect(msg)valuePayload on Ok; raises with msg on Err.
r.unwrap_or(default)valuePayload on Ok; default on Err.
r.map(f)ResultOk(v)Ok(f(v)); Err(e) passes through.
r.map_err(f)ResultErr(e)Err(f(e)); Ok(v) passes through.
r.and_then(f)ResultOk(v)f(v) (expected to return a Result); Err(e) passes through.
print(Ok(5).is_ok())                           // true
print(Err("bad").unwrap_or(0))                 // 0
print(Ok(5).map(fn(v) { return v * 2 }))       // Result::Ok(10)
print(Err("x").map(fn(v) { return v * 2 }))    // Result::Err("x")

fn halve(x) {
  if x % 2 == 0 { return Ok((x / 2).to_int()) }
  return Err("odd")
}
print(Ok(8).and_then(halve).and_then(halve))   // Result::Ok(2)

Iter methods — iter🔗

An iter is Bop’s lazy iterator. Values you can iterate over — arrays, strings, dicts, built-in iterators, and user-defined containers — all participate in the same protocol:

  1. v.iter() returns an iterator.
  2. it.next() advances it, returning Iter::Next(value) or Iter::Done.

for x in v uses this protocol, so anything with a working .iter() method works with for.

MethodReturnsDescription
it.next()Iter::Next(v) / Iter::DoneAdvance by one. Cloning an iterator shares its cursor (like Python / Rust / JS) — two names pointing at the same iterator advance together.
it.iter()iterReturns the same iterator. Makes for x in it work whether it is already an iterator or a fresh iterable.
let it = [10, 20, 30].iter()
print(it.type())                // "iter"
print(it.next())                // Iter::Next(10)
print(it.next())                // Iter::Next(20)

for x in it { print(x) }        // 30  (picks up from the current cursor)

User-defined iterables🔗

A struct can participate in the iterator protocol by implementing .iter() (and, if it’s its own iterator, .next()):

struct Bag { items }
fn bag_of(arr) { return Bag { items: arr } }
fn Bag.iter(self) { return self.items.iter() }   // delegate to the backing array

let b = bag_of(["x", "y", "z"])
for v in b { print(v) }                           // x  y  z

That’s the minimal shape. A container that wraps an array and delegates .iter() is the 80% case. User types with genuine internal state (like a lazy counter) work the same way — define fn Counter.iter(self) to return an iterator (either the backing data’s iterator, or self if self also has .next()).

The Iter enum🔗

.next() returns one of two variants of the built-in Iter enum — always in scope, no use required:

enum Iter {
  Next(value),
  Done,
}

Pattern-match directly:

let it = [1, 2].iter()
let r = it.next()
print(match r {
  Iter::Next(v) => "got: " + v.to_str(),
  Iter::Done    => "exhausted",
})
// got: 1

Struct / enum methods🔗

User-declared. See Structs & Enums. Method dispatch on a struct tries the universal common methods first, then looks up fn TypeName.method declared in the same module.

Same-module rule: user-declared methods must live in the module that declares the type. You can’t extend a type imported from another module, and you can’t add methods to the built-ins (int, string, array, Result, Iter, …). See Methods must live in the type’s own module for the rationale and the “use a free function instead” workaround.

Module methods🔗

If you use path as m, m is a Value::Module. m.type()"module", m.inspect()"<module path>". Otherwise . on a module accesses its exports:

use std.math as m
print(m.PI)             // exported constant
print(m.type())         // "module"   (universal method, not an export)
Found something unclear?Edit this page on GitHub
Documentation

Search Bop

Start typing to search the guide and reference.