← Back to README | Language Guide | API Reference | Architecture →
Quick reference for Carv's syntax and features. This is a work in progress - things might change as I figure stuff out.
Use f"..." for interpolated strings:
let name = "Carv";
let version = "0.2.0";
println(f"Welcome to {name} v{version}!");
// expressions work too
let x = 10;
let y = 5;
println(f"{x} + {y} = {x + y}");
// function calls
println(f"Length of name: {len(name)}");
Use {{ and }} to escape braces: f"Use {{braces}} like this".
let x = 10; // immutable
mut y = 20; // mutable
y = 30; // ok
const PI = 3.14159; // constant
Mutable variables support compound assignment operators:
mut x = 10;
x += 5; // 15
x -= 3; // 12
x *= 2; // 24
x /= 4; // 6
x %= 4; // 2
x &= 3; // bitwise AND
x |= 8; // bitwise OR
x ^= 5; // bitwise XOR
Basic types: int, float, string, bool, char, void
let name: string = "hello";
let count: int = 42;
let pi: float = 3.14;
let flag: bool = true;
let c: char = 'a';
Type inference works most of the time so you can skip annotations.
Carv has a move-based ownership system. Some types are copy types (implicitly copied), others are move types (ownership is transferred).
Copy types are implicitly copied on assignment: int, float, bool, char, and references (&T, &mut T).
let x = 42;
let y = x; // x is copied, both x and y are valid
print(x); // OK
print(y); // OK
Move types transfer ownership on assignment: string, []T (arrays), {K:V} (maps), and class instances.
let s = "hello";
let t = s; // s is moved into t, s is now invalid
// print(s); // ERROR: use of moved value 's'
print(t); // OK: "hello"
mut x = "world";
x = "new"; // old value dropped, new value assigned
References allow temporary access without transferring ownership. Carv enforces borrow rules at compile time.
Use &x to create an immutable borrow:
fn print_len(s: &string) -> int {
return len(s);
}
let msg = "hello";
print_len(&msg); // immutable borrow — msg still valid
print(msg); // OK
Use &mut x to create a mutable borrow:
fn append_excl(s: &mut string) {
*s = *s + "!";
}
mut greeting = "hi";
append_excl(&mut greeting); // mutable borrow
print(greeting); // "hi!"
- One mutable XOR many immutable: You can have either one mutable borrow OR multiple immutable borrows, but not both.
- No borrow escape: References cannot be returned from functions or stored in fields.
- Can't move while borrowed: You cannot move or reassign a value while it is borrowed.
let s = "hello";
let r = &s;
// let t = s; // ERROR: cannot move 's' while it is borrowed
print(len(r)); // OK: 5
Use *x to dereference a reference:
fn increment(x: &mut int) {
*x = *x + 1;
}
mut n = 5;
increment(&mut n);
print(n); // 6
Interfaces define a set of methods that types can implement. Dispatch is dynamic via vtable.
interface Printable {
fn to_string(&self) -> string;
fn display(&self);
}
Interface methods must declare a receiver: &self (immutable borrow) or &mut self (mutable borrow). Value receivers (self) are not supported in v1.
Use impl Interface for Type:
class Person {
name: string
}
impl Printable for Person {
fn to_string(&self) -> string {
return self.name;
}
fn display(&self) {
println(self.name);
}
}
The checker verifies that all interface methods are implemented with matching signatures.
Cast a class reference to an interface reference with as:
let p = new Person;
p.name = "Alice";
let item: &Printable = &p as &Printable;
item.display(); // dynamic dispatch through vtable
Only &Interface (immutable) and &mut Interface (mutable) are supported — owned trait objects are not.
The as keyword is an infix operator for type casts:
let x = &p as &Printable; // immutable interface ref
let y = &mut p as &mut Printable; // mutable interface ref
let nums = [1, 2, 3, 4, 5];
let first = nums[0];
let length = len(nums);
Hash maps with curly brace syntax:
let scores = {"alice": 100, "bob": 85, "charlie": 92};
// access
print(scores["alice"]); // 100
// check if key exists
if has_key(scores, "dave") {
print("found dave");
}
// get keys and values
let names = keys(scores); // ["alice", "bob", "charlie"]
let points = values(scores); // [100, 85, 92]
// immutable operations (return new maps)
let updated = set(scores, "dave", 88);
let removed = delete(scores, "bob");
Keys must be hashable (strings, integers, booleans).
fn add(a: int, b: int) -> int {
return a + b;
}
fn greet(name: string) {
print("Hello, " + name);
}
// anonymous functions
let double = fn(x: int) -> int { return x * 2; };
Carv supports async fn and await. Async functions are compiled into state machines in C codegen.
async fn fetch_data() -> int {
return 42;
}
async fn carv_main() -> int {
let v = await fetch_data();
println(v);
return 0;
}
awaitcan only be used insideasync fn.awaitexpects an async/future-producing expression.- Async locals captured across suspension points are stored in generated async frames.
- For compiled async programs (
carv build), useasync fn carv_main() -> intas entrypoint.
This is my favorite feature. Pass results through a chain of functions:
// instead of: print(add(5, double(x)))
x |> double |> add(5) |> print;
// with arrays
[1, 2, 3] |> head |> print; // 1
The left side becomes the first argument of the right side.
if x > 0 {
print("positive");
} else if x < 0 {
print("negative");
} else {
print("zero");
}
// c-style for
for (let i = 0; i < 10; i = i + 1) {
print(i);
}
// for-in (arrays)
for item in [1, 2, 3] {
print(item);
}
// for-in (strings — iterates characters)
for ch in "hello" {
print(ch);
}
// for-in (maps — iterates keys)
for key in {"a": 1, "b": 2} {
print(key);
}
// while
while condition {
// ...
}
// infinite loop
for {
if done {
break;
}
}
class Counter {
value: int = 0
fn increment() {
self.value = self.value + 1;
}
fn get() -> int {
return self.value;
}
}
let c = new Counter;
c.increment();
print(c.get()); // 1
Carv has a Rust-inspired module system using require:
// import entire module
require "./utils";
// import with alias
require "./utils" as u;
// import specific exports
require { add, subtract } from "./math";
// import all exports
require * from "./math";
// import builtin stdlib module
require "net" as net;
Use pub to mark functions, classes, constants, and variables as public:
// math.carv
pub fn add(a: int, b: int) -> int {
return a + b;
}
pub fn multiply(a: int, b: int) -> int {
return a * b;
}
// private - not exported
fn helper() {
// ...
}
pub const PI = 3.14159;
pub let VERSION = "0.2.0";
Initialize a project with carv init:
myproject/
├── carv.toml # project config
├── src/
│ └── main.carv # entry point
└── carv_modules/ # dependencies (future)
[package]
name = "myproject"
version = "0.1.0"
entry = "src/main.carv"
[dependencies]
# future: external packages
[build]
output = "build"
optimize = trueFor error handling without exceptions:
fn divide(a: int, b: int) -> Result {
if b == 0 {
return Err("cannot divide by zero");
}
return Ok(a / b);
}
// pattern matching
let result = divide(10, 2);
match result {
Ok(v) => print(v),
Err(e) => print("error: " + e),
}
// try operator (early return on error)
fn calculate() -> Result {
let x = divide(10, 2)?; // unwraps Ok or returns Err
let y = divide(x, 3)?;
return Ok(y);
}
print(...)/println(...)- print to stdoutlen(x)- length of string or arraystr(x)- convert to stringint(x)- convert to intfloat(x)- convert to floattype_of(x)- get type as string
push(arr, item)- return new array with item appendedhead(arr)- first elementtail(arr)- all elements except first
split(str, sep)- split string into arrayjoin(arr, sep)- join array into stringtrim(str)- remove whitespacesubstr(str, start, end?)- substringcontains(str, substr)- check if containsstarts_with(str, prefix)- check prefixends_with(str, suffix)- check suffixreplace(str, old, new)- replace all occurrencesindex_of(str, substr)- find index (-1 if not found)to_upper(str)/to_lower(str)- case conversionord(char)- character to ASCII codechr(int)- ASCII code to characterchar_at(str, idx)- get character at index
parse_int(str)- parse string as integerparse_float(str)- parse string as float
keys(map)- get all keys as arrayvalues(map)- get all values as arrayhas_key(map, key)- check if key existsset(map, key, value)- return new map with key setdelete(map, key)- return new map with key removed
read_file(path)- read file contentswrite_file(path, content)- write to fileappend_file(path, content)- append to filefile_exists(path)- check if file existsmkdir(path)- create directoryremove_file(path)- delete filerename_file(old_path, new_path)- rename/move fileread_dir(path)- list directory entriescwd()- current working directory
- Import built-in module:
require "net" as net;(orrequire "web" as web;) net.tcp_listen(host, port)- create listener, return handlenet.tcp_accept(listener)- accept connection, return handlenet.tcp_read(conn, max_bytes)- read bytes, return stringnet.tcp_write(conn, data)- write string, return bytes writtennet.tcp_close(handle)- close listener/connection handle
args()- get CLI argumentsexec(cmd, ...args)- run command, return exit codeexec_output(cmd, ...args)- run command, returnOk(stdout)/Err(stderr)getenv(key)- get environment variablesetenv(key, value)- set environment variable
exit(code?)- exit programpanic(msg)- crash with message
- Semicolons are required at the end of statements
- The language is statically typed but has decent inference
- Map and array builtins (
push,set,delete) return new values (originals unchanged) - Mutable variables (
mut) support direct mutation via index/field assignment - Error handling uses Result types instead of exceptions
← Back to README | Language Guide | API Reference | Architecture →