Culebra Quick Guide ==================== One file with what cannot be looked up: the syntax, the habits from other languages that do *not* carry over, and the signatures of the library a program reaches for without being told to. The reference set it condenses ([`handbook.md`](handbook.md), [`language.md`](language.md), [`stdlib.md`](stdlib.md)) is around 150k tokens; this is the part that fits in a prompt. The subsystems it leaves out — 3D, 2D, sockets, parsers, full-text search — are named in section 4 with the one command that prints each one whole, because a signature list is not enough to drive them and reading the chapter is. Every ` ```culebra ` block below is executed by `culebra test --doc docs`, so it cannot drift from the implementation. A line ending in `# => ` is verified stdout; `# !! ` is a verified throw. Section 4 is generated from the reference docs by `just gen-quick-guide` — do not edit it by hand. Contents -------- 1. [Running a program](#1-running-a-program) 2. [Syntax](#2-syntax) 3. [What does not carry over](#3-what-does-not-carry-over) 4. [Signature index](#4-signature-index) 5. [Templates](#5-templates) ## 1. Running a program ```bash culebra prog.cul # the bytecode VM (default) culebra --jit prog.cul # LLVM ORC JIT — identical output culebra build prog.cul -o prog # AOT, self-contained binary culebra test # run every test_*.cul below the cwd culebra fmt -i . # format in place (no style options) culebra lint . # static checks; exit 1 warn, 2 error culebra docs -g 'Math.wrap' # look a signature up in the reference culebra docs stdlib Scene # print one namespace's chapter whole ``` Source files use the `.cul` extension. There is no project file, no manifest, and no package manager: the whole standard library is in scope without an `import`. Undefined names are rejected before the program runs, so a wrong library guess fails immediately rather than halfway through: ```culebra # !! undefined variable 'puts' puts('hi') ``` That check covers names, not members: `Math.abss(1)` and `xs.len()` survive `lint` and fail when the line runs, and a missing property is `nil` rather than an error. So run what you write — a program that has only been linted has not been checked. The whole reference set is inside the binary, always matching the build being run. `culebra docs -g ` prints the sections that match and exits 1 when none do, which settles whether an API exists without reading the output. Patterns are identifiers or phrases, not questions. ## 2. Syntax ### 2.1 Bindings Bindings are immutable unless declared `mut`. A bare `x = ...` introduces a binding, or reassigns the nearest enclosing one. ```culebra x = 1 # immutable binding, or reassign an outer one let y = 2 # immutable; must not shadow an outer binding mut z = 3 # mutable z = 4 # bare reassignment z += 1 # also -= *= /= %= **= @= &= |= ^= <<= >>= inspect(z) # => 5 ``` Parameters are immutable too — assigning to one is an error, not a local copy: ```culebra bump = fn (n) { n += 1 n } inspect(try { bump(1) } catch e { e.kind }) # => 'ImmutableError' ``` Introducing a binding that would shadow a *captured outer* variable is a compile-time error. Block-local rebinding inside one function is fine. ### 2.2 Types ```culebra inspect(type_of(nil)) # => 'Nil' inspect(type_of(true)) # => 'Bool' inspect(type_of(42)) # => 'Long' inspect(type_of(3.14)) # => 'Float' inspect(type_of('hi')) # => 'String' inspect(type_of([1, 2])) # => 'Array' inspect(type_of({a: 1})) # => 'Object' inspect(type_of(fn () { 1 })) # => 'Function' inspect(type_of((1, 'a'))) # => 'Tuple' inspect(type_of(Set(1, 2))) # => 'Set' inspect(type_of('hello'.slice(1, 3))) # => 'StringView' ``` `Tensor` is the twelfth (section 4). `Object` is the substrate for classes, modules and errors. ### 2.3 Control flow `if`, `match` and `cond` are expressions; `while` and `for` are statements. ```culebra n = 7 sign = if n > 0 { 1 } else if n < 0 { -1 } else { 0 } inspect(sign) # => 1 label = match n { 0 => 'zero', k if k < 10 => 'small', _ => 'large', } inspect(label) # => 'small' grade = cond { # a subjectless match n >= 90 => 'A', n >= 5 => 'B', _ => 'C', } inspect(grade) # => 'B' inspect(n > 5 ? 'big' : 'small') # => 'big' ``` ```culebra for i in 0..3 { inspect(i) } # exclusive; 0..=2 inclusive; `by k` steps # => | # 0 # 1 # 2 for k, v in {a: 1, b: 2} { inspect("{k}={v}") } # => | # 'a=1' # 'b=2' ``` `break` and `continue` work in both loops. Either loop may carry a label, and `break` / `continue` may name one to leave (or advance) that loop instead of the innermost — the label goes on the same line as the keyword, and names no variable. A `nobreak` block runs only if the loop was not broken out of. `while`, `if` and `match` accept an init clause scoped to the construct: ```culebra for v in [1, 3, 5] { if v % 2 == 0 { break } } nobreak { inspect('all odd') # => 'all odd' } while mut i = 0; i < 3 { i += 1 } if let m = 6; m > 5 { inspect('big') } # => 'big' search: for row in [[1, 2], [3, 4]] { for cell in row { if cell == 3 { inspect(cell) # => 3 break search } } } ``` ### 2.4 Functions ```culebra add = fn (a, b) { a + b } inspect(add(2, 3)) # => 5 typed = fn (a: Long, b: Long) -> Long { a + b } inspect(typed(2, 3)) # => 5 square = |x| x * x # a lambda body is a single expression by default inspect(square(7)) # => 49 inspect([1, 2, 3].map(|x| x * 2)) # => [2, 4, 6] inspect([[1, 2]].map(fn ((a, b)) { a + b })) # => [3] ``` Bind a literal like any other value (`name = fn (...) { ... }`), or use the declaration form `fn name(...) { ... }`. Pass a callback inline as `|x|`; braces also work — `|x| { ... }` accepts statements, just like `fn (x) { ... }`. For a named function — one you are going to call by name, especially a recursive one — prefer the declaration form. Only it can be a [generator](#26-iterators) or take part in [multimethod dispatch](#29-classes-ufcs-multimethods-traits), and only it gives `fn.name` a source-level name; the two forms are otherwise interchangeable, and inside either one a bare `fn` in the body calls the function itself: ```culebra fn fib(x) { if x < 2 { x } else { fn(x - 2) + fn(x - 1) } } inspect(fib(10)) # => 55 inspect(fib.name) # => 'fib' ``` A block evaluates to its last expression, so `return` is rarely needed. `*` makes the rest keyword-only, `**rest` collects unknown keywords, `*rest` collects extra positionals: ```culebra greet = fn (name, *, greeting = 'hi', **opts) { suffix = opts.has('loud') && opts.loud ? '!' : '' "{greeting}, {name}{suffix}" } inspect(greet('alice')) # => 'hi, alice' inspect(greet('bob', greeting: 'yo')) # => 'yo, bob' inspect(greet('cy', loud: true)) # => 'hi, cy!' inspect(greet('dee', **{greeting: 'hey'})) # => 'hey, dee' ``` ### 2.5 Strings Double quotes interpolate; single quotes are literal. ```culebra name = 'Culebra' inspect("hello, {name}") # => 'hello, Culebra' inspect('hello, {name}') # => 'hello, {name}' inspect('a' + 'b') # => 'ab' ``` `size()` counts UTF-8 bytes; `for` and `iter()` step one Unicode scalar; `graphemes()` steps one user-perceived character. There is no `s[i]` operator — use `slice`, which takes byte offsets. ```culebra inspect('café'.size()) # => 5 inspect('café'.graphemes().collect().size()) # => 4 inspect('café'.slice(0, 1)) # => 'c' inspect('hello world'.split(' ')) # => ['hello', 'world'] inspect(['a', 'b'].join('-')) # => 'a-b' ``` `"""` is a block string: it interpolates like `"..."`, strips the indentation of its closing delimiter, and drops the newline before it. The closing `"""` must be on its own line. Use it for anything multi-line rather than concatenating `\n`. ```culebra sql = """ SELECT * FROM t """ inspect(sql.lines()) # => ['SELECT *', 'FROM t'] ``` ### 2.6 Iterators `range` is lazy, `iota` allocates. `.iter()` makes an Array lazy; chains stop at the first consumer and build no intermediates. ```culebra inspect(iota(3)) # => [0, 1, 2] inspect(range(1000).filter(|x| x % 2 == 0).map(|x| x * 3).take(4).collect()) # => [0, 6, 12, 18] inspect(range(1, 11).reduce(0, |a, x| a + x)) # => 55 inspect([1, 2, 3, 4].iter().zip(['a', 'b']).collect().size()) # => 2 for i, v in ['x', 'y'].enumerate() { inspect("{i}:{v}") } # => | # '0:x' # '1:y' ``` A function whose body contains `yield` is a generator; calling it returns an iterator. ```culebra fn countdown(start) { mut i = start while i > 0 { yield i i -= 1 } } inspect(countdown(3).collect()) # => [3, 2, 1] ``` A method that yields makes its object iterable (`iter() { for x in self.items { yield x } }`). Any object with `iter()`, `has_next()` and `next()` works with `for` and with every chain method. ### 2.7 Pattern matching ```culebra describe = fn (v) { match v { 0 => 'zero', 1 | 2 | 3 => 'small', ..0 => 'negative', n: Long if n > 100 => "big ({n})", n: Long => "int ({n})", s: String => "str ({s})", [] => 'empty', [x] => "one ({x})", [head, ...tail] => "head={head} rest={tail.size()}", {name} => "named {name}", _ => 'other', } } inspect(describe(2)) # => 'small' inspect(describe(-2.5)) # => 'negative' inspect(describe(999)) # => 'big (999)' inspect(describe([1, 2, 3])) # => 'head=1 rest=2' inspect(describe({name: 'z'})) # => 'named z' ``` A range pattern (`..0`, `1..=9`, `0.5..`) matches any number inside the interval, `Long` or `Float` — the values `(1..=9).contains(x)` accepts. There is no exhaustiveness check; supply a `_` arm. A bare name is read by its first letter: uppercase is a type test that binds nothing (`String => …`, `Shape.Origin => …`), any other name binds the value. To compare with a constant, use a guard (`n if n == LIMIT`). The same patterns bind (`let (a, b) = pair`); without `let` they assign to *existing* variables, right-hand side evaluated first — `(a, b) = (b, a)` swaps, `(x, y) = match … { … }` writes both. ### 2.8 Errors, `defer`, `drop` Any value can be thrown, and `try` is an expression. ```culebra check = fn (x) { if x < 0 { throw "negative: {x}" } x } inspect(try { check(-1) } catch e { e }) # => 'negative: -1' inspect(try { check(7) } catch _ { 0 }) # => 7 ``` What follows `catch` is a pattern, as in a `match` arm, and a `try` takes several clauses: `catch e` takes whatever was thrown, a clause that tests takes only what it matches, and a value no clause takes keeps propagating. ```culebra class Timeout { new(.after) {} } wait = fn (v) { try { throw v } catch t: Timeout if t.after > 10 { 'slow' } catch Timeout { 'timeout' } catch IOError { 'io' } catch e { "other: {e}" } } inspect(wait(Timeout(30))) # => 'slow' inspect(wait(Timeout(1))) # => 'timeout' inspect(wait('x')) # => 'other: x' ``` Built-in errors are Objects carrying a `kind`, and the kind is a type name that takes an Object carrying it (`catch IOError`, `e: TypeError`): ```culebra inspect(try { 1 / 0 } catch e { e.kind }) # => 'ZeroDivisionError' ``` `defer` runs on every exit path from the enclosing block, in LIFO order. An Object with a no-arg `drop` property has it called when the last reference goes away. ```culebra { defer { inspect('second') } defer { inspect('first') } inspect('body') } # => | # 'body' # 'first' # 'second' ``` ```culebra { r = {drop: fn () { inspect('released') }} inspect('in scope') } inspect('after') # => | # 'in scope' # 'released' # 'after' ``` ### 2.9 Classes, UFCS, multimethods, traits Fields are reached through `self` (not `this`). Calling the class is shorthand for `.new`. A class can define `drop()` for RAII cleanup, the same way an object literal does (2.8) — it fires when the last reference to an instance goes away. ```culebra class Car { wheels = 4 # declared field with a default new(.mpr, .wheels?) { # field parameters: stored into self.mpr / self.wheels self.miles = 0 } drop() {} # cleanup resources when the last reference goes away run(n) { self.miles += self.mpr * n } get far() { self.miles > 10 } # computed property, no parentheses static unit() { Car(1) } } c = Car(5) c.run(3) inspect(c.miles) # => 15 inspect(c.far) # => true inspect(c.wheels) # => 4 inspect(type_of(c)) # => 'Car' ``` A `new` parameter written `.x` also stores into the field `x` (declaring it if the body does not); `.x?` is optional and takes the declaration's default, so `Car(5, wheels: 3)` overrides the 4. Operators map to dunder methods (`__add__`, `__eq__`, `__lt__`, `__index__`, `__setindex__`, `__call__`, ...). Reverse-side methods (`__radd__`) do not exist — put the overload on the type that owns the operation. Any free function `f(x, ...)` can be called as `x.f(...)`, but an existing property or method always wins, and a first parameter whose annotation rejects `x` takes `f` out of the running: ```culebra double = fn (x) { x * 2 } inspect(42.double()) # => 84 ``` Several free functions may share a name and dispatch on their declared parameter types: ```culebra class Circle { new(r) { self.r = r } } fn area(c: Circle) { 3 * c.r * c.r } fn area(n: Long) { n } inspect(area(Circle(2))) # => 12 inspect(area(10)) # => 10 ``` A `trait` is structural: any class with matching method names and arities conforms, with no `impl` declaration. Trait methods may carry a default body, and `@derive(Eq, Hash, Show, Comparable)` generates the usual conformance methods. ```culebra trait Greeter { hello() -> String } class Bob { new(n) { self.n = n } hello() { "hi, {self.n}" } } greet = fn (g: Greeter) -> String { g.hello() } inspect(greet(Bob('Ann'))) # => 'hi, Ann' ``` An `enum` is a sum type: nullary variants are singleton values, payload variants are constructors, and `match` takes them apart. A variant is `Eq` and `Hashable` as-is, so it is an Object / Set key. ```culebra enum Shape { Circle(Float), Rect(Float, Float), Origin, } area = fn (s) { match s { Circle(r) => 3 * r * r, Rect(w, h) => w * h, Origin => 0, } } inspect(area(Shape.Rect(2.0, 3.0))) # => 6.0 mut seen = {} seen[Shape.Origin] = 'origin' inspect(seen[Shape.Origin]) # => 'origin' inspect(Set(Shape.Origin, Shape.Origin).size()) # => 1 ``` ### 2.10 Effects `perform` invokes an operation whose meaning the enclosing `handle` decides. A continuation is multi-shot. ```culebra effect fn ask() inspect(handle { perform ask() * 2 } with ask(resume) { resume(21) }) # => 42 ``` A clause that never calls `resume` discards the rest of the computation — that is exactly an exception. `with return(v) { ... }` maps the normal-completion value. ### 2.11 Modules `export` and `import` are top-level only, so the dependency graph is known at parse time. ```culebra # lib.cul let greet = fn (n) { "hello, {n}" } export {greet} ``` ```culebra # doctest: skip # main.cul import lib from './lib.cul' inspect(lib.greet('world')) # => 'hello, world' ``` The path is a single-quoted literal resolved relative to the importing file. Each module is evaluated once; a cycle is an `ImportError`. ### 2.12 Optional type annotations Annotations are checked at three boundaries — assignment, parameter passing, and return — and nowhere else. `Long | String` is a union, `T?` is `T | Nil`, and `Array` documents the element type without checking it elementwise. ```culebra show = fn (x: Long | String) -> String { to_string(x) } inspect(show(1)) # => '1' inspect(show('hi')) # => 'hi' ``` ## 3. What does not carry over Each row is a habit from another language that fails, or silently produces something else, in Culebra. | Reaching for | Culebra | |---|---| | `'text {x}'` interpolating | only `"..."` interpolates; `'...'` is literal | | `puts` / `console.log` / `print(x)` adding a newline | `inspect(x)` quoted debug form, `println(x)` raw + newline, `print(x)` raw | | `this` | `self` | | `x \|> f()` | UFCS: `x.f()`. There is no pipeline operator | | `{1, 2}` / `{3}` as a Set | Braces are an Object's. A Set is `Set(1, 2)`, `Set(x)`, `Set()` | | `a \| b`, `a & b` on sets | methods: `a.union(b)`, `a.intersect(b)`, `a.diff(b)` | | `{a: 1} + {b: 2}` | `TypeError`. There is no Object merge operator | | `s[0]` on a String | `TypeError`. Use `s.slice(0, 1)`, which takes byte offsets | | `-7 % 3 == 2` (Python) | `-1` — the sign follows the dividend, as in C | | `-7 / 2 == -4` (Python) | `-3` — Long division truncates toward zero | | `if c { x = 1 } else { x = 2 }` then reading `x` | `NameError` at load: each arm is a scope of its own. Write `x = if c { 1 } else { 2 }` | | `if [] { }`, `if '' { }` | `TypeError`. Only `Bool`, `Long` and `Float` are testable | | `0 == false` | `false` — there is no cross-type coercion | | `.length` / `.count` | `.size()`. A missing property is `nil`, so `.length` reads as `nil` instead of raising | | `.append(x)` | `.push(x)` | | `xs.sort(\|a, b\| ...)` — a comparator | `xs.sorted_by(key)` takes a *key*, not a comparator; `reverse: true` descends. A Tuple key orders by several things: `xs.sorted_by(\|x\| (x.a, x.b))`. There is no `<=>` | | `xs.take(n)` / `xs[:n]` | `xs.slice(0, n)`. `take` is an Iterator method — `xs.iter().take(n).collect()` | | `obj.items()` / `obj.entries()` / `obj.to_array()` | `for k, v in obj`, or `obj.keys().map(\|k\| (k, obj[k]))` | | `del a[i]` / `a.splice(i, 1)` | `a.remove_at(i)`, which returns the removed element | | `obj['missing']` | `KeyError`. `obj.missing` is `nil`; `obj.get('missing', dflt)` takes a fallback | | `'ab' * 3` | `TypeError`. There is no string repetition operator | | `s.find(x)` / `s.indexOf(x)` | `s.index_of(x)` — a byte offset, `-1` when absent | | `s.ljust(10)` / `s.padStart(10)` / `s.zfill(5)` | the interpolation format spec: `"{s:<10}"`, `"{n:05}"` | | `s.split()` with no argument, splitting on whitespace | `s.split_whitespace()`. `split` always takes a separator | | assigning to a parameter | parameters are immutable — `ImmutableError` | | `x = 1` twice in one scope | `ImmutableError`. Declare `mut x = 1` | | `and` / `or` / `not` | `&&` / `\|\|` / `!` | | `elif` | `else if` | | `#` only, or `//` only, for comments | both work, plus `/* ... */` | | `async` / `await` | absent by design — I/O blocks; use `Isolate` / `Parallel` | | a package manager | the whole standard library is in scope with no `import` | Three that yield a value rather than an error, and so are easy to miss: ```culebra # split returns StringView, not String — cheap, but type_of differs inspect(type_of('a,b'.split(',')[0])) # => 'StringView' # a missing property is nil, silently inspect([1, 2].length) # => nil inspect([1, 2].size()) # => 2 # a match no arm covers is nil, not an error — and `lint` only says so # when the subject is an enum, so give every other match a `_` arm inspect(match 9 { 0 => 'zero', }) # => nil ``` ### Idioms The rows above fail loudly. These ones run — they are just not how the language is written, and nothing will tell you so. Prefer the right column. Porting from another language: its `if/elif/else` or `case`/`switch` chains map onto `match`/`cond` here, not onto a literal `if`/`else if` chain — carrying the source's control-flow shape over verbatim is the single most common transfer mistake. | Works, but | Write | |---|---| | `if c { a } else { b }` as a value | `c ? a : b` | | `if` / `else if` chain yielding a value | one value against constants: `match`; unrelated conditions: `cond` | | `ClassName.new(...)` | `ClassName(...)` — a `class`-defined type is callable and desugars to its constructor | | `new(x, y) { self.x = x; self.y = y }` | `new(.x, .y) {}` — a field parameter stores into its field | | `"a" + x + "b"` (splicing literal text around a value) | `"a{x}b"` | | `i = i + 1` | `i += 1` | | `x.size() == 0` / `> 0` | `x.empty()` / `!x.empty()` | | `mut i = 0` alongside a loop, `i += 1` | `for i, v in xs.enumerate()` | | `mut out = []` + `for` + `out.push(f(x))` | `xs.map(f)` — same for `filter` | | `mut t = {}` + `for` + `t[k] = v` | `xs.map(\|x\| (k(x), v(x))).to_object()` | | `mut found = false` + `while !found` | `for x in xs { … break }`, or `xs.find(p)` | | `mut hit = false` + `for` + `if p(x) { hit = true; break }` | `xs.any(p)` (`xs.all(p)` for the inverse) | | `"a\n" + "b\n"` | a `"""` block | | `.map(fn (x) { expr })` | `.map(\|x\| expr)` | | `range(0, n)` | `range(n)` | | `iota(n).map(\|_\| v)` | `repeat(n, v)` | | `for i in 0..xs.size() { xs[i] … }` | `for x in xs` | | `mut i = start; while i < end { …; i += 1 }` | `for i in start..end { … }` | | `match n { k if k < 10 => … }` / `x >= lo && x < hi` | `match n { ..10 => … }` — a range pattern; `(lo..hi).contains(x)` for the test alone | | `{k1: v1, k2: obj.k2, k3: obj.k3}` (copy every field to change one) | `{...obj, k1: v1}` | | `if cond { stmt }` — single statement, no `else` | `stmt if cond` (`stmt unless cond` for the negation) | | `if x == nil { x = v }` / `if !d.has(k) { d[k] = v }` | `x ??= v` / `d[k] ??= v` — also targets `obj.key` | | `"{a.b(c).d ?? e}"` — a long/nested expression inline | `let x = a.b(c).d ?? e`, then `"{x}"` | `cond` is a `match` with no subject, so a chain of unrelated tests is `cond { a > 1 => …, b < 2 => …, _ => … }`. A loop that must report whether it finished takes a `nobreak` block instead of a flag. This holds for every constructor the library documents, not just the types written in Culebra: `Scene.Image(4, 4)` and `Channel()` are the same calls as `Scene.Image.new(4, 4)` and `Channel.new()`. The index in section 4 still spells each one `.new(...)`, since that is the method being described. ## 4. Signature index Receivers are named by convention: `s` a String, `a` an Array, `o` an Object, `it` an Iterator, `re` a compiled Regex, `f` an open File. An entry with no receiver at all (`contains(x)` under **Set methods**, `json()` under **Http**) is a method on that group's own type. A group marked **(experimental)** is opt-in at build time and may be missing from a released binary; its section in [`stdlib.md`](stdlib.md) says so. The namespaces in the table at the end are named, not listed. Each drives a stateful subsystem — a view and a frame loop, an open connection, a grammar, an emitter — where the signatures say what to call but not in what order, against what, or what owns the result. `culebra docs stdlib ` prints one of those chapters whole, intro and subsections together. Read it before writing against the namespace; guessing from a signature there produces code that lints and does not run. **String methods** — s.size() -> Long; s.empty() -> Bool; s.presence() -> String | StringView | Nil; s.upper() -> String; s.lower() -> String; s.capitalize() -> String; s.title() -> String; s.normalize(form: StringLike = "NFC") -> String; s.eq_ignore_case(other: StringLike) -> Bool; s.reverse() -> String; s.repeat(n: Long) -> String; s.truncate(max: Long, ellipsis: StringLike = "...") -> String; s.trim() -> String; s.trim_start(chars: StringLike = "") -> String; s.trim_end(chars: StringLike = "") -> String; s.tr(from: StringLike, to: StringLike) -> String; s.split(sep: StringLike, limit: Long = 0) -> Array; s.rsplit(sep: StringLike, limit: Long = 0) -> Array; s.split_whitespace() -> Array; s.split_once(sep: StringLike) -> Tuple | Nil; s.rsplit_once(sep: StringLike) -> Tuple | Nil; s.split_iter(sep: StringLike) -> Iterator; s.lines() -> Array; s.replace(pat: String | Regex, repl: String | Function) -> String; s.contains(sub: StringLike) -> Bool; s.count(sub: StringLike) -> Long; s.starts_with(prefix: StringLike) -> Bool; s.ends_with(suffix: StringLike) -> Bool; s.index_of(sub: StringLike, start: Long = 0) -> Long; s.last_index_of(sub: StringLike) -> Long; s.strip_prefix(prefix: StringLike) -> String; s.strip_suffix(suffix: StringLike) -> String; s.replace_first(pat: String | Regex, repl: String | Function) -> String; s.is_digit() -> Bool; s.is_alpha() -> Bool; s.is_alnum() -> Bool; s.is_space() -> Bool; s.is_ascii() -> Bool; s.slice(start: Long, end: Long) -> StringView; s.view() -> StringView; s.to_string() -> String; s.iter() -> Iterator; s.code_points() -> Iterator; s.graphemes() -> Iterator; s.words() -> Iterator; s.sentences() -> Iterator; s.bytes() -> Iterator; String.from_code_point(cp: Long) -> String; String.from_code_points(cps: Array) -> String; String.from_bytes(bytes: Array) -> String; String.pack(type: String | Class, values: Array, endian: String = "little") -> String; String.unpack(type: String | Class, data: StringLike, at: Long = 0, count: Long? = nil, into: Array? = nil, endian: String = "little") -> Array; String.pack_size(type: String | Class) -> Long **Array methods** — a.size() -> Long; a.empty() -> Bool; a.presence() -> Array | Nil; a.push(x: Any) -> Nil (mutating); a.pop() -> Any (mutating); a.extend(other: Array) -> Nil (mutating); a.insert(i: Long, x: Any) -> Nil (mutating); a.remove_at(i: Long) -> Any (mutating); a.get(i: Long, fallback: Any) -> Any; a.slice(start: Long, end: Long) -> Array; a.join(sep: String) -> String; a.contains(v: Any) -> Bool; a.index_of(v: Any) -> Long; a.reverse() -> Nil (mutating); a.map(f: Function) -> Array; a.filter(f: Function) -> Array; a.for_each(f: Function) -> Nil; a.reduce(init: Any, f: Function) -> Any; a.find(f: Function) -> Any; a.any(f: Function) -> Bool; a.all(f: Function) -> Bool; a.flat_map(f: Function) -> Array; a.sum() -> Long | Float; a.product() -> Long | Float; a.min() -> Any; a.max() -> Any; a.min_by(f: Function) -> Any; a.max_by(f: Function) -> Any; a.to_set() -> Set; a.to_object() -> Object; a.group_by(f: Function) -> Object; a.partition(p: Function) -> Tuple; a.unzip() -> Tuple; a.sort(reverse: Bool = false) -> Nil (mutating); a.sorted(reverse: Bool = false) -> Array; a.sort_by(key: Function, reverse: Bool = false) -> Nil (mutating); a.sorted_by(key: Function, reverse: Bool = false) -> Array **Object methods** — o.size() -> Long; o.empty() -> Bool; o.presence() -> Object | Nil; o.keys() -> Array; o.values() -> Iterator; o.has(key: String) -> Bool; o.get(key, fallback) -> Any; o.get_or_put(key, init) -> Any (mutating); o.remove(key: String) -> Nil (mutating) **Set methods** — size(); empty() -> Bool; presence(); contains(x) -> Bool; union(b); intersect(b); diff(b); sym_diff(b); subset(b) -> Bool; superset(b) -> Bool; to_array(); iter(); add(x); remove(x) **Iterator methods** — iter(); it.map(f); it.filter(p); it.take(n); it.skip(n); it.take_while(p); it.skip_while(p); it.step_by(n); it.distinct(); it.tap(f); it.scan(init, f); it.flatten(); it.chunk_by(f); it.chunks(n); it.windows(n); it.flat_map(f); it.chain(other); it.zip(other); it.enumerate(); it.collect(); it.join(sep); it.for_each(f); it.reduce(init, f); it.find(p); it.any(p); it.all(p); it.count(); it.first(); it.last(); it.nth(n); it.position(p); it.contains(v); it.sum(); it.product(); it.min(); it.max(); it.min_by(f); it.max_by(f); it.to_set(); it.to_object(); it.group_by(f); it.partition(p); it.unzip() **Core built-ins** — to_long(v: Any, *, base: Long = 10) -> Long; to_float(v: Any) -> Float; to_string(v: Any) -> String; class_of(v: Any) -> Object?; type_of(v: Any) -> String; range(n: Long, *, step: Long = 1) -> Iterator; range(start: Long, end: Long, *, step: Long = 1) -> Iterator; iota(n: Long) -> Array; iota(start: Long, end: Long) -> Array; repeat(n: Long, value: Any) -> Array; grid(x_range: Range, y_range: Range) -> Iterator **Math** — Math.pi; Math.e; Math.inf; Math.nan; Math.abs(x: Long|Float) -> Long|Float; Math.min(a, b, ...) -> Long|Float; Math.max(a, b, ...) -> Long|Float; Math.log(x: Long|Float) -> Float; Math.exp(x: Long|Float) -> Float; Math.sqrt(x: Long|Float) -> Float; Math.sin(x) -> Float; Math.cos(x) -> Float; Math.tan(x) -> Float; Math.asin(x) -> Float; Math.acos(x) -> Float; Math.atan(x) -> Float; Math.atan2(y, x) -> Float; Math.floor(x: Long|Float) -> Long; Math.ceil(x: Long|Float) -> Long; Math.round(x: Long|Float) -> Long; Math.f32(x: Long|Float) -> Float; Math.pow(base: Long, exp: Long) -> Long; Math.sign(x: Long) -> Long; Math.clamp(x: Long|Float, lo: Long|Float, hi: Long|Float) -> Long|Float; Math.wrap(x: Long, n: Long) -> Long **IO** — IO.inspect(x: Any) -> Nil; IO.print(x: Any) -> Nil; IO.println(x: Any = '') -> Nil; IO.input() -> String; IO.stdin() -> reader; .read(); .read(n: Long); .lines(); IO.einspect(x: Any) -> Nil; IO.eprint(x: Any) -> Nil; IO.eprintln(x: Any) -> Nil; IO.stdin_is_terminal() -> Bool; IO.stdout_is_terminal() -> Bool; IO.stderr_is_terminal() -> Bool; IO.capture(f: Function) -> String **FS** — FS.join(parts...: String) -> String; FS.sep() -> String; FS.basename(path: String) -> String; FS.dirname(path: String) -> String; FS.extension(path: String) -> String; FS.stem(path: String) -> String; FS.read(path: String) -> String; FS.write(path: String, content: String) -> Nil; FS.exists(path: String) -> Bool; FS.is_file(path: String) -> Bool; FS.is_dir(path: String) -> Bool; FS.size(path: String) -> Long; FS.list_dir(path: String) -> Array; FS.mkdir(path: String) -> Nil; FS.temp_dir() -> String; FS.mkdtemp(prefix: String) -> String; FS.remove(path: String, recursive: Bool = false) -> Nil; FS.rename(src: String, dst: String) -> Nil; FS.copy(src: String, dst: String, recursive: Bool = false) -> Nil; FS.chmod(path: String, mode: Long) -> Nil; FS.chown(path: String, owner = nil, group = nil) -> Nil; FS.stat(path: String) -> Object; FS.walk(path: String) -> Array; FS.glob(pattern: String) -> Array; FS.watch(path: String, recursive: Bool = true, match: Array? = nil) -> WatchHandle; FS.abspath(path: String) -> String; FS.realpath(path: String) -> String; FS.normpath(path: String) -> String; FS.is_abs(path: String) -> Bool; FS.symlink(target: String, link: String) -> Nil; FS.readlink(path: String) -> String; FS.is_symlink(path: String) -> Bool; p.join(other); p.resolve(); p.exists(); p.is_file(); p.is_dir(); p.read(); p.write(content); p.mkdir(); p.remove(recursive=false); p.rename(dst); p.list(); p.glob(pattern); p.walk(); p.str() **File** — File.open(path: String, mode: String = "r") -> File; File.with(path: String, mode: String = "r", fn: Function) -> Any; f.read() -> String; f.read(n: Long) -> String; f.lines() -> Iterator; f.chunks(n: Long) -> Iterator; f.write(data: String) -> Nil; f.flush() -> Nil; f.seek(offset: Long, whence: String = "set") -> Nil; f.tell() -> Long; f.close() -> Nil **Time** — Time.now() -> Instant; Time.monotonic() -> Float; Time.sleep(secs: Float) -> Nil; Time.from_iso(s: String) -> Instant; Time.from_unix(secs: Long|Float) -> Instant; Time.from_parts(p: Object, utc: Bool = false) -> Instant; Time.parse(s: String, fmt: String) -> Instant; t.iso(utc: true) -> String; t.format(fmt: String, utc: false) -> String; t.parts(utc: false) -> Object; t.weekday(utc: false) -> Long; t.add(years=0, months=0, days=0, hours=0, minutes=0, seconds=0, utc: false) -> Instant; t.start_of(unit: String, utc: false) -> Instant; t.unix() -> Float; t.unix_nanos() -> Long; d.seconds() / .milliseconds() / .minutes() / .hours() / .days() -> Float; d.abs() -> Duration **Random** — Random.seed(n: Long) -> Nil; Random.int(lo: Long, hi: Long) -> Long; Random.uniform(lo: Float, hi: Float) -> Float; Random.gauss(mu: Float, sigma: Float) -> Float; Random.shuffle(a: Array) -> Nil; Random.weighted_choice(pop: Array, weights: Array) -> Any; Random.choice(pop: Array) -> Any **Sys** — Sys.exit(code: Long) -> Nil; Sys.env(name: String, fallback = '') -> Any; Sys.set_env(name: String, value: String) -> Nil; Sys.getcwd() -> String; Sys.chdir(path: String) -> Nil; Sys.data_dir(app: String) -> String; Sys.time() -> Float **Tensor** — Tensor.zeros(...) -> Tensor; Tensor.ones(...); Tensor.randn(...); Tensor.from(arr: Array) -> Tensor; Tensor.concat(parts: Array, axis: Long = 0) -> Tensor; Tensor.where(cond: Tensor, a, b) -> Tensor; Tensor.adam_step(p, m, v, g, lr, beta1, beta2, eps, step) -> Nil; Tensor.index_add(indices: Tensor, values: Tensor, target_shape: Array) -> Tensor; Tensor.scatter_to_axis(indices: Tensor, values: Tensor, size: Long) -> Tensor; Tensor.from_csv(path: String) -> Tensor; Tensor.eval(t1, t2, ...) -> Nil; .shape() -> Array; .dot(other: Tensor) -> Tensor; .linear_sigmoid(x, b) -> Tensor; .pow(exp) -> Tensor; .gt(other) / .lt(other) / .ge(other) / .le(other) / .eq(other) / .ne(other) -> Tensor; .tanh() / .sin() / .cos() -> Tensor; .clamp(lo, hi) -> Tensor; .rope(pos: Long, base) -> Tensor; .causal_attention(k, v) -> Tensor; .layer_norm(gamma, beta) -> Tensor; .transpose() -> Tensor; .permute(axes: Array) -> Tensor; .slice(start, end) -> Tensor; .narrow(params: Array) -> Tensor; .reshape(dims: Array) -> Tensor; .unfold(params: Array) -> Tensor; .pad(params: Array) -> Tensor; .fold(params: Array) -> Tensor; .sum() -> Float; .sum(axis: Long?, keepdims: Bool = false) -> Tensor; .mean() / .mean(axis, keepdims:); .max() / .max(axis, keepdims:); .argmax(axis: Long, keepdims: Bool = false) -> Tensor; .index_select(indices: Tensor) -> Tensor; .softmax_cross_entropy(targets: Tensor) -> Tensor; .to_array() -> Array; .item() -> Float; .requires_grad() -> Tensor; .backward() -> Nil; .grad() -> Tensor; .zero_grad() -> Nil; .detach() -> Tensor; Tensor.use_cpu() -> Nil; Tensor.use_gpu() -> Nil; Tensor.use_auto() -> Nil; Tensor.gpu_available() -> Bool; Tensor.device() -> String; Tensor.profile(fn) -> Array; Tensor.profile_scope(label, fn) -> Any **JSON** — JSON.stringify(v, indent=0, sort_keys=false, lines=false) -> String; JSON.parse(s, lines=false, number_mode='auto', jsonc=false) -> Any **Args** — Args.parse(argv: Array, spec: Object) -> Object; Args.try_parse(argv, spec) -> Object; Args.help(spec: Object) -> String **Proc** — Proc.run(cmd: Array, cwd=nil, env=nil, stdin="", check=false, timeout=0, share=nil, inherit_env=true) -> Object; Proc.all(commands: Array>, limit: Long = , timeout: Long = 0, fail_fast: Bool = false, retries: Long = 0, share: Object? = nil, inherit_env: Bool = true) -> Array; Proc.race(commands: Array>, share: Object? = nil, inherit_env: Bool = true) -> Object; Proc.spawn(cmd: Array, cwd=nil, env=nil, stdin="", share=nil, inherit_env=true) -> handle; h.wait(); h.poll(); h.kill(sig = 15) **Isolate** — Isolate.spawn(fn, *args) -> handle; h.join(); h.poll(); h.drop(); Channel.new(cap = 1) -> (tx, rx); tx.send(v); tx.clone(); rx.clone(); tx.drop(); rx.drop(); rx.recv(); rx.try_recv(); rx.drain(max = nil); Channel.fan_in(items: [rx]) -> rx; Channel.fan_in(items, fn) -> rx; Parallel.map(items, fn, limit = ); Parallel.each(items, fn, limit = ); Parallel.map_settled(items, fn, limit = ); Parallel.race(items, fn, limit = ); Signal.notify(tx); Signal.reset(); SharedBuffer.new(count, type: Class) -> buffer; SharedBuffer.file(path, count, type: Class) -> buffer; SharedBuffer.shared(count, type: Class) -> buffer; buffer.with_lock(fn) **Matchers** — assert_true(x: Bool, label: String? = nil) -> Nil; assert_false(x: Bool, label: String? = nil) -> Nil; assert_eq(a, b, label: String? = nil) -> Nil; assert_ne(a, b, label: String? = nil) -> Nil; assert_lt(a, b, label: String? = nil) -> Nil; assert_le(a, b, label: String? = nil) -> Nil; assert_gt(a, b, label: String? = nil) -> Nil; assert_ge(a, b, label: String? = nil) -> Nil; assert_throws(kind: String, f: Function) -> Nil; assert_close(a: Float, b: Float, tol: Float, label: String? = nil) -> Nil **Regex** — Regex.compile(pattern) -> Regex; Regex.compile(pattern, flags) -> Regex; Regex.escape(s) -> String; Regex.interp(x) -> String; Regex.find(pattern, s); Regex.match(pattern, s); Regex.find_all(pattern, s); Regex.test(pattern, s); Regex.split(pattern, s); Regex.replace_all(pattern, s, repl) -> String; Regex.replace_first(pattern, s, repl) -> String; re.test(s) -> Bool; re.find(s); re.match(s); re.find_all(s) -> [Match]; re.find_all_str(s) -> [String]; re.find_all_index(s) -> [Int]; re.count(s) -> Int; re.find_iter(s) -> Iterator; re.replace_all(s, repl) -> String; re.replace_first(s, repl) -> String; re.split(s) -> [String] **Http** — json(); Http.get(url, headers=nil, timeout=0, follow_redirects=true); Http.delete(url, headers=nil, timeout=0, follow_redirects=true); Http.head(url, headers=nil, timeout=0, follow_redirects=true); Http.post(url, body="", content_type="text/plain", headers=nil, timeout=0, follow_redirects=true); Http.put(url, body="", content_type="text/plain", headers=nil, timeout=0, follow_redirects=true); Http.request(method, url, body="", content_type="text/plain", headers=nil, timeout=0, follow_redirects=true); Http.sse(url, on_event, headers=nil, timeout=0, follow_redirects=true); Http.client(base_url, headers=nil, timeout=0, follow_redirects=true); Http.server(); Http.ws(url); Http.sse(url, on_event, headers=nil, timeout=0, follow_redirects=true) -> Object; Http.client(base_url, headers=nil, timeout=0, follow_redirects=true) -> Object; Http.server() -> Object; static(mount, dir); sink.write(chunk); bind(port, host="0.0.0.0") -> Long; serve(workers=0); serve_async(workers=0); listen(port, host="0.0.0.0", workers=0); listen_async(port, host="0.0.0.0", workers=0) -> Long; stop(); close(); ws.receive(); ws.try_receive(); ws.send(msg); ws.set_timeout(ms); ws.close(); ws.is_open(); Http.ws(url) -> Object **Encoding** — Encoding.html; Encoding.html.escape(s) -> String; Encoding.html.unescape(s) -> String; Encoding.base64; Encoding.base64.encode(s) -> String; Encoding.base64.decode(s) -> String; Encoding.hex; Encoding.hex.encode(s) -> String; Encoding.hex.decode(s) -> String; Encoding.url; Encoding.url.encode(s) -> String; Encoding.url.decode(s) -> String; Encoding.utf16; Encoding.utf16.encode(s: String, bom: Bool = false) -> String; Encoding.utf16.decode(bytes: String) -> String **Compress** — Compress.gzip(data: String) -> String; Compress.gunzip(data: String) -> String; Compress.deflate(data: String, level: Long = -1) -> String; Compress.zip(files: Object | Dir) -> String **Hash** — Hash.sha256(data: String) -> String; Hash.sha1(data: String) -> String; Hash.sha512(data: String) -> String; Hash.md5(data: String) -> String; Hash.hmac_sha256(key: String, data: String) -> String; Hash.hmac_sha1(key: String, data: String) -> String; Hash.hmac_sha512(key: String, data: String) -> String **CSV** — CSV.parse(text, delimiter=",", header=false, types=nil) -> Array; CSV.stringify(rows: Array, delimiter: String = ",") -> String **Env** — Env.parse(text: String) -> Object; Env.load(path: String = ".env", override: Bool = false) -> Object **UUID** — UUID.v4() -> String; UUID.v7() -> String **Term** — Term.fg(s, n) -> String; Term.bg(s, n) -> String; Term.rgb(s, r, g, b) -> String; Term.red(s); Term.bold(s); Term.dim(s); Term.underline(s); Term.reverse(s); Term.style(fg = nil, bg = nil, bold = false, dim = false, underline = false, reverse = false) -> String; Term.clear() -> String; Term.move(x, y) -> String; Term.hide(); Term.show(); Term.cols(); Term.rows() -> Long; Term.size() -> (Long, Long); Term.width(s) -> Long; Term.flush(); screen.size(); cols(); rows(); screen.clear(); screen.set(x, y, glyph, style = ""); screen.put(x, y, s, style = ""); screen.render() -> String; screen.flush(); screen.poll(timeout) -> Object? **Log** — Log.debug(msg: String, fields: Object = {}); Log.info(msg, fields = {}); Log.warn(msg, fields = {}); Log.error(msg, fields = {}); Log.with(fields: Object) -> logger; Log.set_level(level: String) -> Nil; Log.set_format(format: String) -> Nil **TOML** — TOML.parse(text: String) -> Object; TOML.stringify(v: Object, sort_keys: Bool = false) -> String **XML** — XML.parse(text, keep_space=false) -> Object; XML.events(text, keep_space=false) -> Iterator; el.find(path, namespaces={}) -> Object | Nil; el.find_all(path, namespaces={}) -> Array; el.find_iter(path, namespaces={}) -> Iterator; el.text(path=".", namespaces={}) -> String | Nil; el.stringify(indent=0, declaration=false) -> String; XML.find(el, path, namespaces={}) -> Object | Nil; XML.find_all(el, path, namespaces={}) -> Array; XML.find_iter(el, path, namespaces={}) -> Iterator; XML.text(el, path=".", namespaces={}) -> String | Nil; XML.stringify(el, indent=0, declaration=false) -> String **Vector2** — Vector2.new(x, y); a.hash() -> Long; a.dot(other); a.length(); a.length_squared(); a.normalized(); a.distance_to(other); a.distance_squared_to(other) -> Float; to_string(a) -> String **Vector3** — Vector3.new(x, y, z); a.hash() -> Long; a.dot(other); a.length(); a.length_squared(); a.normalized(); a.distance_to(other); a.distance_squared_to(other) -> Float; to_string(a) -> String **Deque** — Deque.new(); d.push_back(x); d.push_front(x); d.pop_back() -> Any; d.pop_front() -> Any; d.peek_back() -> Any; d.peek_front() -> Any; d.size(); d.empty(); d.to_array() -> Array; d.iter() -> Iterator; to_string(d) **PriorityQueue** — PriorityQueue.new(*, key: Function | Nil = nil, reverse: Bool = false); pq.push(x); pq.pop() -> Any; pq.peek() -> Any; pq.size(); pq.empty(); to_string(pq) -> String **Audio** — Audio.available() -> Bool; sound.play(); sound.stop(); sound.playing() -> Bool; sound.volume(v); sound.pitch(p); sound.pan(p); music.play(); music.stop(); music.pause(); music.resume(); music.seek(seconds); music.playing() -> Bool; music.volume(v); music.pitch(p); music.pan(p); pcm.ready() -> Bool; pcm.needed() -> Long; pcm.push(samples: Array) -> Long; pcm.submit() -> Long; pcm.latency() -> Float; pcm.play(); stop(); pause(); resume(); playing() -> Bool; pcm.volume(v); pcm.pitch(p); pcm.pan(p); Audio.capture_available() -> Bool; mic.ready() -> Bool; mic.start(); running() -> Bool; mic.waiting() -> Long; mic.read(frames = nil) -> Array; Audio.Kauai.new(text: String, voices = nil, drums = nil); Audio.Kauai.load(path: String, voices = nil, drums = nil, dir = nil) **Dir** — Dir.disk(path: String | Path) -> Dir; Dir.memory(files: Object) -> Dir; Dir.embedded(name: String) -> Dir; Dir.zip(path: String | Path = nil, *, bytes: String = nil) -> Dir; d.read(path); d.list_dir(path); d.exists(path); d.is_file(path); d.is_dir(path); d.size(path); d.files(); d.glob(pattern); d.copy_to(dest) ### Read the chapter for these | Namespace | Signatures | What it is | |---|---:|---| | `SQLite` | 10 | embedded SQL database (query / execute / prepared statements / transactions) | | `Canvas` | 61 | immediate-mode 2D framebuffer for games (shapes, sprites, offscreen targets, text, keys/mouse/gamepad, window controls) | | `Scene` | 178 | retained-mode 3D renderer for procedural geometry | | `Net` | 22 | raw TCP / UDP sockets and name resolution (the layer under `Http`) | | `Desktop` / `Webview` | 10 | native WebView desktop app: local HTTP server + window, one call | | `PEG` | 33 | PEG parser generator: write a grammar, get a syntax tree | | `CodeGen` | 144 | build a small language's IR by hand and run it | | `StateMachine` | 8 | hierarchical state machine, with a text DSL | | `FST` | 14 | compiled read-only dictionary: prefix, predictive and fuzzy search | | `Search` | 9 | full-text index over your own documents, ranked | ## 5. Templates ### CLI tool ```culebra # doctest: skip spec = { name: 'greet', args: [ {name: 'who', doc: 'who to greet'}, {name: 'times', type: 'Long', default: 1, doc: 'repeat count'}, ], } args = Args.parse(Sys.argv, spec) for _ in range(args.times) { println("hello, {args.who}") } ``` ### HTTP request ```culebra # doctest: skip r = Http.get('https://example.com/api', headers: {Accept: 'application/json'}) if !r.ok { throw "HTTP {r.status}: {r.reason}" } println(r.json().title) ``` ### File processing ```culebra # doctest: skip counts = File.with('input.txt', 'r', fn (f) { mut n = {} for line in f.lines() { for w in line.trim().lower().split(' ') { k = w.to_string() n[k] = n.get(k, 0) + 1 } } n }) for k in counts.keys().sorted() { println("{k}\t{counts[k]}") } ``` ### Test file ```culebra # doctest: skip # test_math.cul @test fn adds() { assert_eq(1 + 2, 3) } @parametrize([(1, 2, 3), (10, 20, 30)]) fn adds_each(a, b, want) { assert_eq(a + b, want) } ``` Where to go next ---------------- - Tutorial with rationale: [`handbook.md`](handbook.md) - Formal grammar and evaluation rules: [`language.md`](language.md) - Full API reference with prose: [`stdlib.md`](stdlib.md) - Binary builds and embedding: [`deployment.md`](deployment.md)