Skip to content

Variables and Types

lash has two declaration keywords: let for immutable bindings and mut for mutable ones.

let name = "alice"
name = "bob" # Error: 'name' is immutable. Declare with 'mut' if you need to reassign.
mut counter = 0
counter = counter + 1 # valid

Redeclaring a variable in the same scope is an error. Use drop to remove it first:

let x = 5
let x = 10 # Error: 'x' is already declared in this scope. Use 'drop x' first.
let y = 5
drop y
let y = 10 # valid -- 'y' was dropped

Mutable variables can change type on reassignment:

mut val = 42
val = "now a string" # valid

Variables are dynamically typed. lash supports six core types and one specialized type:

TypeExample
stringlet greeting = "hello"
intlet count = 42
floatlet ratio = 3.14
boollet verbose = true
listlet files = ["a.txt", "b.txt"]
objectlet config = { editor: "vim", tabs: 4 }
semverlet ver = semver("1.2.3")

Arithmetic follows expected promotion rules — mixed int/float operations promote to float. Integer division truncates:

let a = 10 / 3 # 3 (int)
let b = 10.0 / 3 # 3.333... (float)
let c = 10 % 3 # 1 (modulo)

Lists can hold mixed types:

let ports = [8080, 8443, 9090]
let mixed = ["hello", 42, true]

Objects use colon syntax for key-value pairs:

let server = { host: "localhost", port: 8080, tls: false }

Access object properties with dot or bracket syntax:

server.host # "localhost"
server["port"] # 8080
let key = "tls"
server[key] # false

Ranges produce integer lists:

let digits = 1..10 # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Ranges work anywhere a list is expected, including for loops and method chains.

Double-quoted strings support four interpolation forms:

let user = "alice"
let count = 3
echo "hello $user" # hello alice
echo "${user}_home" # alice_home
echo "files: $(ls | wc -l)" # files: 42
echo "total: $((count * 10))" # total: 30

Single-quoted strings are literal — no interpolation:

echo 'no $interpolation here' # no $interpolation here

String concatenation uses the ~ operator:

let full = "hello" ~ " " ~ "world"
let path = HOME ~ "/documents"

Wrap an expression in parentheses to print its value to stdout:

let x = 42
(x) # outputs: 42
(x * 2) # outputs: 84
([1, 2, 3]) # outputs: [1, 2, 3]

Without parentheses, a bare name in statement position is treated as a command.

Variables are lexically scoped to the block where they are declared (function body, if/for/while block, or session-level). Inner scopes can shadow outer variables without modifying them:

let x = "outer"
if true {
let x = "inner"
echo $x # inner
}
echo $x # outer

Subshells inherit a copy of the parent scope. Modifications do not propagate back.

All let and mut variables are automatically exported to child processes. Objects and lists are serialized as JSON strings when exported.

System environment variables like HOME and PATH are mutable by default. Use the env namespace for explicit access:

echo $HOME # standard interpolation
let editor = env.EDITOR # explicit namespace access

Every value has .isSuccess and .isFailure properties:

let result = `grep -r "TODO" src/`
if result.isSuccess {
echo "found matches"
}

Every type also defines .min and .max bounds (e.g., int.min, int.max).