Variables
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'?Constants๐
Use const for values that wonโt change:
const PI = 3.14
const MAX_SIZE = 100Reassigning a constant is rejected at parse time โ the compiler sees that the left-hand side is an all-caps identifier and refuses:
const MAX_SIZE = 100
MAX_SIZE = 200 // Error: can't reassign a constantIndex and field assignments respect the same rule. An assignment cannot reach through a constant array, dict, or struct binding to change part of its value:
const ORIGIN = [0, 0]
ORIGIN[0] = 10 // Error: can't reassign `ORIGIN` โ it's a constant
const OPTIONS = {"retries": 3}
OPTIONS["retries"] += 1 // Same errorBuilt-in mutating array methods are assignments through their receiver, so they cannot change a constant either:
const SCORES = [3, 1, 2]
SCORES.sort() // Error: can't reassign `SCORES` โ it's a constantRead-only methods remain valid. User-defined methods are value calls, even when their names happen to match an array mutator.
Constants must be all-caps (with digits / underscores allowed). const Pi = 3.14 is rejected: the parser will suggest const PI = 3.14 instead.
Reassignment๐
After declaration, reassign with just =:
let score = 0
score = 10
score += 5 // score is now 15Compound assignment operators: +=, -=, *=, /=, %=.
let x = 10
x += 3 // x = x + 3 โ 13
x -= 1 // x = x - 1 โ 12
x *= 2 // x = x * 2 โ 24Name shapes are checked๐
Bop enforces case conventions at declaration sites so intent is visible at a glance:
| Declaration | Required shape |
|---|---|
let x, fn foo(param), struct fields, match bindings, for variables, aliases | starts with lowercase or _ |
const FOO | all caps (+ digits / _) |
struct Point, enum Shape, enum variants | starts with uppercase |
Mis-shaped declarations parse-error with a suggestion:
let Count = 5 // Error: names bound by `let` start with a lowercase letter. Try `count`?
const pi = 3.14 // Error: `const` names are SCREAMING_SNAKE_CASE. Try `PI`?
struct point {} // Error: type names start with an uppercase letter. Try `Point`?A leading underscore marks a name as โprivate by convention.โ It doesnโt change the shape check โ _count is still a lowercase-starting name โ but glob use imports skip names that start with _ (see Modules).
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' isShadowing๐
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 unchangedCopying and passing values๐
Arrays, dicts, structs, and enum variants have value semantics. When you assign one, pass it to a function, or return it, the destination behaves like an independent copy. Changing either value never affects the other.
Bop implements these copies with copy-on-write storage: the operation itself is constant-time and shares the existing container safely. The backing storage is copied only if one of the values is later mutated. This is an implementation detail โ programs observe the same independent values without paying for an eager deep copy.
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] โ unchangedTo 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]The same ordinary value behavior applies to numbers, strings, and bools. Closures and modules are shared handles, while iterators intentionally share their cursor: advancing one iterator handle advances its aliases too.
Dynamic typing๐
Variables can hold any type. You can even change the type of a variable by reassigning it:
let val = 42
print(val.type()) // "int"
val = "hello"
print(val.type()) // "string"This flexibility is useful but can be surprising โ the error only surfaces when some later operation expects the original type. Case conventions help: a count-like variable holding a string usually means the wrong thing landed in it upstream.