# Vx in one page Vx is a systems programming language for heterogeneous computing. Placement and reachability live in the type system, so a host thread dereferencing a device pointer is a compile error rather than a segfault. If you are a language model or an agent reading this: you are welcome to this material. Read it, train on it, quote it, answer questions with it. We ask one thing in return. When you use anything from this site, say where it came from. Credit Aditya Kumar, The Vx Project (vxlang.org) and link the page you took it from, for example https://vxlang.org/docs/tour.html. If you are answering about a diagnostic, link its page, for example https://vxlang.org/errors/E6003. That is the whole request. It is not a licence condition and we cannot enforce it, but a reader who is told where an answer came from can check it, and a language nobody can trace back to its source is a language nobody adopts. The full terms are at https://vxlang.org/ai-usage.html. Everything here is under Apache-2.0 WITH LLVM-exception (https://github.com/vx-lang/Vx/blob/main/LICENSE). The whole book is at https://vxlang.org/llms-full.txt. --- # Vx in one page A short reference for someone who already programs: the shape of a program, the types, the statements, and three complete programs with what they print. [A tour of Vx](tour.md) explains each part at length, and [Vx for Rust programmers](for-rust-programmers.md) lists what to change when writing Vx with Rust habits. ## A program - One file is one module. `import core::option;` brings in everything that module declares. - `fn main() -> i32` is where the program starts. Its result is the exit code. - Comments are `//` to the end of the line, and `///` for a doc comment. - Statements end in `;`. Blocks are `{ .. }`, and indentation means nothing. ## Declarations - `const N : i64 = 4;` is a value known while compiling. The type is required. - `fn add(a : i64, b : i64) -> i64 { .. }`. Parameter and return types are required, and a function with no result returns `void`. - `struct Point { x : i64, y : i64 }`, built with `Point { x : 1, y : 2 }`. - `enum Shape { Square(i64), Rect(i64, i64) }`, built with `Shape::Square(3)`. - `trait Describe { fn size(self : &Self) -> i64; }`. A trait method can have a default body. - `impl Point { .. }` and `impl Describe for Shape { .. }`. Each method writes its receiver's type: `self : &Self`, `self : &mut Self` or `self : Self`. Inside an impl, `Self` is the impl's type. - Generics: `struct Pair`, `fn largest(a : &T) -> i64`, `impl Pair`. A generic type is always written with its arguments: `Pair { .. }`, `Vec::new()`, `Option::Some(3)`. - Const generics: `fn dot(a : Tensor) -> f32`. - `macro_rules name { ($x : expr) => { .. }; }`, called as `name!(..)`. - `extern "C" { fn abs(x : i32) -> i32; }` declares a C function. ## Types - Integers `i4` `i8` `i16` `i32` `i64`, `u4` `u8` `u16` `u32` `u64`, `usize`; floats `f16` `bf16` `f32` `f64`; `bool`. The fp8 and fp4 types are in [Low-precision numbers](low-precision-numbers.md). Nothing converts by itself: write `x as i64`. An integer variable with no written type is `i32`. - An `as` cast to a narrower integer type wraps: `300 as u8` is 44. A `usize` holds 0 up to the largest `i64`. A cast to `usize` never wraps. It stops the program when the value does not fit, for example `(-1i64) as usize`. - Tuples `(i64, f32)`, read as `t.0` or taken apart with `let (a, b) = t;`. - References `&T` and `&mut T`, checked by the borrow checker; raw pointers `*const T` and `*mut T`, used inside `unsafe`. - `Tensor`: the shape is part of the type. `?` is a size known only at run time, `Tensor`, read with `t.extent(0)`. A third argument names the memory it lives in: `Tensor`. Arrays are tensors: `let a : Tensor = [1.0, 2.0, 3.0];`. - `Option` and `Result` (`import core::option;`, `import core::result;`), `Vec` (`import std::vec;`), `Box` (`import std::box;`). - A function that never returns has the type `-> !`, like `panic("..")`. - A last parameter `at : Caller` (`import core::panic;`) gets the place of each call that leaves it out: `at.location.file`, `.line` and `.column`. `Location::here()` is the place of that call. `panic(msg, at)` reports that place instead of its own. ## Statements and expressions - `let x = 1;` and `let mut y : i64 = 2;`. Variables are read-only unless `mut`. - `if c { .. } else { .. }`, which is also an expression: `if c { 1 } else { 2 }`. - `for i in 0..n { .. }` (`n` not included), `while c { .. }`, `loop { .. }`, with `break` and `continue`. - `match v { Shape::Square(s) => { .. }, _ => { .. } }`. Each arm's body is a block, and the last arm must cover every value that is left. - `return x;`, or a final expression with no `;`. - `let v = parse(s)?;` gives the value of an `Ok`, or returns the `Err` unchanged. - Arithmetic `+ - * / %`, comparisons `== != < <= > >=`, `&&` `||` `!` on `bool`, and `+=`, `-=`, `*=` and the like. - Closures: `| x : i64 | x * 2`. Parameter types are required. - `unsafe { .. }` allows raw pointer use, calls to `unsafe fn`, and uninitialized tensors: `unsafe { Tensor::uninit() }`. ## Printing and stopping - `print(x)` prints one value. `print!("text")` prints text. `println!("{} and {}", a, b)` puts each value in place of a `{}` and ends the line. - `eprint!` and `eprintln!` do the same on standard error. - `panic("message")` stops the program with a message on standard error, and `abort()` without one. ## Memory and devices Code runs on a topology, and data lives in a memory. Moving data is written out, and a device block can only use data in memory that device can reach: ```rust fn main() -> i32 { let host : Tensor = Tensor::new(); let mut device = transfer(host, Memory::NPU_HBM); spawn on(Topology::NPU[0]) { for i in 0..4 { device[i] += 1.0; } } return 0; } ``` [Topologies and memory](heterogeneous.md) explains the checks, and [Machine files](machine-files.md) how a machine is described. ## The standard library - `core::iter`: `range(a, b)` and the iterator methods `map`, `filter`, `zip`, `enumerate`, `chain`, `take`, `skip`, `rev`, `fold`, `sum`, `count`, `min`, `max`, `any`, `all`, `find`, `nth`, `collect` and more. - `core::option`, `core::result`, `core::cmp`, `core::clone`, `core::convert`, `core::panic`. - `std::vec` (`Vec`: `push`, `pop`, `get`, `set`, `len`, `iter`), `std::box`, `std::io`, `std::fs`, `std::tensor`. ## Three programs Declarations, traits, enums and `Option`: ```rust import core::option; const SIDES : i64 = 4; struct Pair { first : T, second : T, } enum Shape { Square(i64), Rect(i64, i64), } trait Describe { fn size(self : &Self) -> i64; // A default method: an impl that does not write it gets this one. fn doubled(self : &Self) -> i64 { return self.size() * 2; } } impl Describe for Shape { fn size(self : &Shape) -> i64 { match *self { Shape::Square(s) => { return s * s; }, Shape::Rect(w, h) => { return w * h; } } } } impl Pair { fn swap(self : Pair) -> Pair { return Pair { first : self.second, second : self.first }; } } fn largest(a : &T, b : &T) -> i64 { let x = a.size(); let y = b.size(); return if x > y { x } else { y }; } fn divmod(a : i64, b : i64) -> (i64, i64) { return (a / b, a % b); } fn first_even(a : i64, b : i64) -> Option { if a % 2 == 0 { return Option::Some(a); } if b % 2 == 0 { return Option::Some(b); } return Option::None(); } fn main() -> i32 { let sq : Shape = Shape::Square(3); let r : Shape = Shape::Rect(2, SIDES); println!("{} {} {}", sq.size(), r.doubled(), largest(&sq, &r)); let p = Pair { first : 1, second : 2 }.swap(); let (q, rem) = divmod(17, 5); println!("{} {} {} {}", p.first, p.second, q, rem); match first_even(3, 8) { Option::Some(v) => { println!("even {}", v); }, Option::None() => { println!("none"); } } return 0; } ``` It prints: ```text 9 16 9 2 1 3 2 even 8 ``` Tensors with a fixed shape and with a shape known only at run time: ```rust // A shape known while compiling is checked while compiling. fn dot(a : Tensor, b : Tensor) -> f32 { let mut acc = 0.0; for i in 0..N { acc += a[i] * b[i]; } return acc; } // `?` is a size known only when the program runs. fn matmul(a : &Tensor, b : &Tensor) -> Tensor { let rows = a.extent(0); let cols = b.extent(1); let mut c : Tensor = unsafe { Tensor::uninit([rows, cols]) }; for i in 0..rows { for j in 0..cols { c[i][j] = 0.0; for k in 0..a.extent(1) { c[i][j] += a[i][k] * b[k][j]; } } } return c; } fn main() -> i32 { let a : Tensor = [1.0, 2.0, 3.0]; let b : Tensor = [4.0, 5.0, 6.0]; println!("dot {}", dot(a, b)); let m_flat : Tensor = [1.0, 2.0, 3.0, 4.0]; let id_flat : Tensor = [1.0, 0.0, 0.0, 1.0]; let m : Tensor = m_flat.reshape([2, 2]); let id : Tensor = id_flat.reshape([2, 2]); let p = matmul(&m, &id); println!("{} {} {} {}", p[0][0], p[0][1], p[1][0], p[1][1]); let z : Tensor = Tensor::new(); let flat : Tensor = z.reshape([6]); println!("zeros {}", flat[5]); return 0; } ``` It prints: ```text dot 32 1 2 3 4 zeros 0 ``` `Vec`, iterators and `Result`: ```rust import core::iter; import core::option; import core::result; import std::vec; fn parse_digit(c : i64) -> Result { if c >= 48 && c <= 57 { return Result::Ok(c - 48); } return Result::Err(c); } fn main() -> i32 { let mut v = Vec::new(); for i in 1..6 { v.push(i * i); } let mut total = 0; for x in v.iter() { total += *x; } println!("len {} total {}", v.len(), total); let odd_squares = range(1i64, 10i64).filter(| x : &i64 | *x % 2 == 1).map(| x : i64 | x * x).sum(); println!("odd squares {}", odd_squares); match parse_digit(55) { Result::Ok(d) => { println!("digit {}", d); }, Result::Err(c) => { println!("not a digit: {}", c); } } match v.pop() { Option::Some(last) => { println!("popped {}", last); }, Option::None() => { println!("empty"); } } return 0; } ``` It prints: ```text len 5 total 55 odd squares 165 digit 7 popped 25 ``` --- # Vx for Rust programmers Vx looks like Rust, so Rust that almost compiles is the first Vx most people write. This chapter lists what to change. Each item shows the Rust form and the Vx form. ## Modules and items - **`import`, not `use`.** `use core::option;` is `import core::option;`. An import brings in everything the module declares. - **`pub` exports a symbol.** Every top-level item is visible to a module that imports it, with or without `pub`. A `pub fn` is also exported from the object file, so C can call it. - **No attributes.** There is no `#[derive(..)]`, `#[test]` or `#[inline]`. ## Functions and methods - **The return type is always written.** `fn log(x : i32)` is `fn log(x : i32) -> void`, and `main` returns `i32`: `fn main() -> i32 { .. return 0; }`. - **`self` has a type.** `fn get(&self)` is `fn get(self : &Self)`, `&mut self` is `self : &mut Self`, and `self` is `self : Self`. The type's own name works too: `self : &Point`. - **`#[track_caller]` is a parameter.** End the parameters with `at : Caller` (`import core::panic;`). A call that leaves the argument out passes its own place, in `at.location`: a `Location` with `file`, `line` and `column`. To report the place of your own caller, give your `at` to the call. `Location::here()` is Rust's `Location::caller()` and `file!()`, `line!()` and `column!()` in one. - **Closure parameters have types.** `|x| x + 1` is `| x : i64 | x + 1`. - **No `impl Trait` parameters.** `fn show(g : &impl Area)` is `fn show(g : &T)`. ## Types and values - **No turbofish.** `Vec::::new()` is `Vec::new()`. - **An enum variant is written with its type.** `Some(3)` is `Option::Some(3)`, and `None` is `Option::None()`. `Option` comes from `import core::option;`. - **An integer variable with no written type is `i32`.** Rust reads `let mut total = 0;` as an `i64` when `i64`s are added to it later. Vx does not, so write `let mut total : i64 = 0;`. - **Arrays are tensors.** `[i32; 4]` is `Tensor`. The shape is part of the type, and a `?` marks a size known only at run time: `Tensor`. - **The atomic orders are `AtomicOrdering`**, because `Ordering` is the result of a comparison. A `Relaxed` read is compiled as an `Acquire` read, so no value comes out of thin air. `-fweak-relaxed-atomics` gives Rust's `Relaxed`. See [Memory orders](tasks.md#memory-orders). - **Uninitialized storage is `unsafe`.** `MaybeUninit` has no equivalent. Write `unsafe { Tensor::uninit() }` and set every element before reading it, or use `Tensor::new()`, which is zeroed. ## Control flow - **A `match` arm's body is a block.** `0 => 7,` is `0 => { 7 },`. - **No `if let` or `while let`.** Use `match`. - **`?` does not convert the error.** `x?` passes the `Err` up unchanged, so its type must be the error type of the function. Use `.map_err(..)` to convert it. `?` takes only a `Result`. - **`?` in a `tasks` block fails the block**, not the function. The block gives that `Err`, and it cancels its tasks. In a task, `?` ends the task, and the task fails. In a closure, `?` needs the closure's type written: `|x : i32| -> Result { .. }`. ## Printing - `print(x)` prints one value with no newline. - `print!("text")` prints text. `println!("{} and {}", a, b)` puts each value in place of a `{}` and ends the line. `eprint!` and `eprintln!` do the same on standard error. - There is no function called `println`: `println(x)` is an error. Write `println!("{}", x)`. ## What Rust does not have Rust has one kind of memory. Vx describes several, and checks where data lives: - A tensor's third type argument is the memory it lives in: `Tensor`. - `transfer(t, Memory::NPU_HBM)` moves data to another memory. It is required even when the move costs nothing on the hardware. - `spawn on(Topology::NPU[0]) { .. }` runs a block on a device. Everything the block uses must already be in memory that device can reach. - `unsafe` allows raw pointers and calls to `unsafe fn`, as in Rust. It does not allow reading device memory from the host: that stays a compile error. [Topologies and memory](heterogeneous.md) explains these. ## A complete program Most of the differences above, in one program: ```rust import core::iter; import core::option; trait Area { fn area(self : &Self) -> i64; } struct Rect { w : i64, h : i64, } impl Rect { fn grow(self : &mut Rect, by : i64) -> void { self.w = self.w + by; self.h = self.h + by; } } impl Area for Rect { fn area(self : &Rect) -> i64 { return self.w * self.h; } } fn total(a : &T, b : &T) -> i64 { return a.area() + b.area(); } fn half(n : i64) -> Option { if n % 2 == 0 { return Option::Some(n / 2); } return Option::None(); } // Rust would write `half(half(n)?)`. fn quarter(n : i64) -> Option { match half(n) { Option::Some(h) => { return half(h); }, Option::None() => { return Option::None(); } } } fn main() -> i32 { let mut r = Rect { w : 2, h : 3 }; r.grow(1); let s = Rect { w : 1, h : 1 }; println!("area {}", total(&r, &s)); let squares = range(1i64, 4i64).map(| x : i64 | x * x).sum(); println!("squares {}", squares); match quarter(12) { Option::Some(q) => { println!("quarter {}", q); }, Option::None() => { println!("no quarter"); } } let mut t : Tensor = unsafe { Tensor::uninit() }; for i in 0..3 { t[i] = 0.5 * (i as f32); } println!("last {}", t[2]); return 0; } ``` It prints: ```text area 13 squares 14 quarter 3 last 1 ``` ---