Skip to content

Basic Concepts

This chapter introduces the core language model you will use across all Shape code.

In Shape, control flow constructs return values.

let score = 84
let grade = if score >= 90 { "A" } else if score >= 80 { "B" } else { "C" }
print(grade)

if, match, blocks, and function bodies are expression-oriented.

Shape has three variable binding forms, each with different ownership behavior:

let name = "Alice" // immutable, uniquely owned — the default
let mut count = 0 // mutable, uniquely owned — for single-owner mutation
count = count + 1
var shared = [1, 2, 3] // mutable, shared — copy-on-write when aliased
  • let is the default. Values are immutable and uniquely owned. Assignment moves the value.
  • let mut is for mutation with a single owner. Mutations happen directly with zero overhead.
  • var is for shared mutable state. Multiple references can exist, and mutations clone the data if needed (copy-on-write).

Use const for compile-time constants:

const MAX_RETRIES = 3
// MAX_RETRIES = 4 // compile error

For full details, see Variables and Types.

Records are typed. Declare a record with type, then construct it with the type name. Fields are checked at compile time.

type Point {
x: int,
y: int,
}
let mut point = Point { x: 10, y: 20 }
point.y = 25
let total = point.x + point.y
print(total)

Anonymous object literals ({ x: 10, y: 20 }) are read-only — they cannot grow new fields at runtime. Use a type declaration when you need field assignment or structural access.

Use fn for named functions.

fn distance2(x: int, y: int) -> int {
x * x + y * y
}
let d2 = distance2(3, 4)
print(d2)

Closures (lambdas) are written with |params| body. Closures pick up their parameter types from the call site they are passed into:

let nums = [1, 2, 3, 4]
let evens = nums.filter(|x| x % 2 == 0) // x: int (inferred from nums)
print(evens)

A bare let f = |a, b| a + b without a call-site context cannot infer a and b — Shape requires types at compile time. Either pass the closure directly into a typed call, or wrap the logic in a named fn.

match is an expression and supports type-based matching.

fn describe(value: int | string) -> string {
match value {
n: int => f"int({n})"
s: string => f"string({s})"
}
}
print(describe(7))
print(describe("hi"))

Shape uses Option<T> for absence and Result<T, E> for failure. Some / None / Ok / Err are available without an explicit import.

fn require_file(name: Option<string>) -> Result<string, string> {
match name {
Some(path) => Ok(path)
None => Err("missing input filename")
}
}
let file = Some("events.csv")
match require_file(file) {
Ok(path) => print(f"path: {path}")
Err(msg) => print(f"error: {msg}")
}
  • ? propagates Option/Result inside functions whose return type matches
  • !! adds error context to propagated failures

See Error Handling for the full operator semantics.

Use named imports with from ... use { ... }:

from std::core::math use { sqrt }
fn radius(area: number) -> number {
sqrt(area / 3.14159)
}
print(radius(100.0))

Bare names come from explicit named imports or the implicit prelude. See Names and Scope for the full target model.

For structured row-oriented data, Shape uses Table<T> with a declared row schema:

type Event {
id: int,
value: number,
}
// Tables are constructed via stdlib loaders (CSV, Arrow, JSON, ...).
// See std::core::csv and std::core::table_methods for the full surface.

Table operations (filter, map, groupBy, orderBy, head/tail) are covered in detail in Data Tables.

Semicolons are usually optional.

  • Use ; when placing multiple expressions on one line.
  • In a block, a trailing ; on the last expression turns it into () (unit) instead of returning that value.
fn a() { 1 } // returns 1
fn b() { 1; } // returns ()

Continue with Variables and Types to learn binding rules, type annotations, and shape-aware object typing in detail.