Language specification
How Caju works: syntax, types, statements, functions, errors and modules. The examples were compiled with version 0.5.0 and show the actual output.
01Overview
Caju is a compiled, statically typed programming language with type inference. The compiler generates x86-64 machine code: ELF executables for Linux and PE for Windows. It also generates WebAssembly modules, which run in Node and in the browser. The compiler and runtime are written in Caju itself.
The language comes with everything you need to work: a compiler, a test runner (caju test) and a package manager (caju pkg), all in a single caju command.
A program is one or more .caju files. Top-level statements run in order, from top to bottom: there is no need to declare a main function.
fn greet(name: str) -> str { return "Hello, " + name + "!"} print(greet("world"))02Lexical elements
Comments. Start with # or // and run to the end of the line.
Statement terminators. A line break or a ; ends a statement. An expression does not continue onto the next line after an operator: split it into variables.
Identifiers. Letters (including accented Latin letters such as é and ç), digits and _, not starting with a digit.
Keywords. These cannot be used as names:
letconstifelsewhilefortoandornottruefalseimportfromexportfnreturnexterndllexportstructinterfacenilswitchcasedefaultbreakcontinuefallthroughdefergoselectchanmaprangeLiterals.
a := 1_000; b := 0xFF; c := 0b1010; d := 0o17print(a, b, c, d, 1.5e3)print("tab\tquote\" \u00e9 \x41", `raw\n`, 'a')03Types
Types are optional in declarations: without an annotation, the compiler infers them. With an annotation, a value of another type is a compile error.
Aliases. byte = uint8, rune = int32, string = str and float64 = num.
Zero value. A variable declared without a value starts with its type's zero value: 0, "", false, nil, or a struct with all fields zeroed.
04Variables and constants
x := 10x = "ten"05Expressions and operators
+ also concatenates strings. There is no ~: to flip the bits, use x ^ -1.
Conversions are written as calls: int(x), num(x), str(x), uint8(x), float32(x), byte(x), rune(x) and so on.
print(2 ** 10, 7 % 3, 6 &^ 3, 1 << 4, -8 >> 1)print(true and not false, !true && false)print(str(3.5), int(3.9), num(2))06Control flow
if accepts a statement before the condition, separated by ;. There are no parentheses around the condition.
Loops. There are four forms of for, plus while:
break and continue accept a label to exit or continue an outer loop.
switch compares against each case, which can list several values. Cases don't fall through to the next one unless they end with fallthrough.
for j = 1 to 2 { print("j", j) } outer: for p := 0; p < 3; p++ { for q := 0; q < 3; q++ { if q == 1 { continue outer } if p == 2 { break outer } print(p, q) }} i := 3switch i {case 1, 2: print("small")case 3: print("three") fallthroughdefault: print("default")}07Functions
Functions are declared with fn at the top level of the file. Every parameter has a type. The result type comes after -> and, if omitted, is inferred from the return.
A function can return multiple values, -> (int, error), and results can be named: a bare return returns their current values.
Inside another function, create functions as values with fn(...) { }. They capture outer variables (closures).
fn split(n: int) -> (q: int, r: int) { q = n / 10 r = n % 10 return}print(split(47)) add := fn(a: int, b: int) -> int { return a + b }print(add(2, 3))... spreads a list into a call, as in append(a, b...).08Structs, methods and interfaces
Structs group typed fields. A literal can give values in order, Point{1, 2}, or by name, Point{y: 2}. Fields left out get their zero value.
Methods are functions with a receiver: fn (p: Point) dist() receives a copy, and fn (p: *Point) move() receives the original and can modify it.
Embedding one struct in another gives the outer struct the inner one's fields and methods.
Interfaces are satisfied implicitly: a type satisfies an interface if it has its methods. An interface can embed another. x.(T) recovers the stored type and, with v, ok := x.(T), does not panic.
interface Named { name() -> str }interface Greeter { Named; greet() -> str } struct P { n: str }fn (p: P) name() -> str { return p.n }fn (p: P) greet() -> str { return "hi " + p.n } gs := []Greeter{P{"ana"}}print(gs[0].greet(), gs[0].name())09Errors, defer and panic
Errors are values of type error. Create one with errors.New("message") or with any type that has an Error() -> str method. Functions that can fail return the error as their last result.
defer schedules a call for when the function returns; deferred calls run from last to first. panic stops execution and unwinds the calls; recover, inside a defer, stops the panic. Runtime errors, such as an index out of range, also panic.
struct NotFound { name: str }fn (e: *NotFound) Error() -> str { return e.name + " not found" } fn find(name: str) -> error { if name != "caju" { return &NotFound{name} } return nil} err := find("mango")if err != nil { print(err.Error()) }10Built-in functions
11Modules and packages
Each file is a module. Only names declared with export can be used by other files.
# mods/geo.cajuexport struct Point { x: int, y: int }export fn origin() -> Point { return Point{} }fn hidden() -> int { return 1 }import "./mods/geo" print(geo.origin())print(geo.hidden())12Package manager
Caju comes with a package manager, caju pkg. There is no central server: a package is a GitHub repository with a caju.toml file at its root. The package name is the repository path, and that is the name programs import.
[package]name = "github.com/acme/strutil"version = "1.1.0"description = "Text helpers"license = "MIT" [dependencies]"github.com/acme/other" = "1.0.0"Versions. A published version is a git tag in the vX.Y.Z format, matching the version in caju.toml. If the two don't match, the package manager rejects the package.
Version selection. Each dependency states the minimum version it needs. When two packages ask for different versions of the same package, the highest of the minimums wins. So the result only changes when a caju.toml asks for a higher version, never because someone published a new one. Versions with different MAJOR numbers, such as 1.x and 2.x, are not compatible.
Version locking. caju.lock records, for every package used directly or indirectly, the version, the commit and a SHA-256 hash of its files. Commit it, so everyone compiles exactly the same files.
Verification. Before compiling, caju build and caju run check that caju.lock is up to date with caju.toml and that each package's files match the locked hash. Any mismatch stops the build:
import "github.com/acme/strutil" print(strutil.shout("caju"))Where they live. Packages are downloaded with git (the version tag) into the cache at ~/.caju/pkg. add, remove and update edit only the [dependencies] section of caju.toml, leaving the rest of the file untouched.
import "github.com/acme/strutil/extra", and a package should only import what is in its own [dependencies].13System calls on Windows
extern declares a function from a Windows DLL, using the stdcall (default) or cdecl calling convention. Parameters use interface types: i32, u32, i64, u64, f32, f64, ptr, bool, str and wstr, and the result can be void. These calls are only available in programs compiled with --target windows.
extern "user32.dll" fn MessageBoxA(hwnd: ptr, text: str, title: str, flags: u32) -> i32 MessageBoxA(0, "Hello from Caju", "caju", 0)14Building and tools
15In progress
- Goroutines, channels and
selectin the native compiler. The keywordsgo,chanandselectare already reserved. - On Windows:
alloc,peek,poke,readstr,dllexportand native DLLs. - Maps with struct, array or
anykeys.