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 explains each part at length, and Vx for Rust programmers 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() -> i32is 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 returnsvoid.struct Point { x : i64, y : i64 }, built withPoint { x : 1, y : 2 }.enum Shape { Square(i64), Rect(i64, i64) }, built withShape::Square(3).trait Describe { fn size(self : &Self) -> i64; }. A trait method can have a default body.impl Point { .. }andimpl Describe for Shape { .. }. Each method writes its receiver’s type:self : &Self,self : &mut Selforself : Self. Inside an impl,Selfis the impl’s type.- Generics:
struct Pair<T>,fn largest<T : Describe>(a : &T) -> i64,impl<T> Pair<T>. A generic type is always written with its arguments:Pair<i64> { .. },Vec<i64>::new(),Option<i64>::Some(3). - Const generics:
fn dot<const N : i32>(a : Tensor<f32, [N]>) -> f32. macro_rules name { ($x : expr) => { .. }; }, called asname!(..).extern "C" { fn abs(x : i32) -> i32; }declares a C function.
Types
- Integers
i8i16i32i64,u8u16u32u64; floatsf16f32f64;bool. Nothing converts by itself: writex as i64. An integer variable with no written type isi32. - Tuples
(i64, f32), read ast.0or taken apart withlet (a, b) = t;. - References
&Tand&mut T, checked by the borrow checker; raw pointers*const Tand*mut T, used insideunsafe. Tensor<f32, [2, 3]>: the shape is part of the type.?is a size known only at run time,Tensor<f32, [?, ?]>, read witht.extent(0). A third argument names the memory it lives in:Tensor<f32, [4], Memory::NPU_HBM>. Arrays are tensors:let a : Tensor<f32, [3]> = [1.0, 2.0, 3.0];.Option<T>andResult<T, E>(import core::option;,import core::result;),Vec<T>(import std::vec;),Box<T>(import std::box;).- A function that never returns has the type
-> !, likepanic("..").
Statements and expressions
let x = 1;andlet mut y : i64 = 2;. Variables are read-only unlessmut.if c { .. } else { .. }, which is also an expression:if c { 1 } else { 2 }.for i in 0..n { .. }(nnot included),while c { .. },loop { .. }, withbreakandcontinue.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;.- Arithmetic
+ - * / %, comparisons== != < <= > >=,&&||!onbool, and+=,-=,*=and the like. - Closures:
| x : i64 | x * 2. Parameter types are required. unsafe { .. }allows raw pointer use, calls tounsafe fn, and uninitialized tensors:unsafe { Tensor<f32, [4]>::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.panic("message")stops the program with a message, andabort()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:
fn main() -> i32 {
let host : Tensor<f32, [4]> = Tensor<f32, [4]>::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 explains the checks, and Machine files how a machine is described.
The standard library
core::iter:range(a, b)and the iterator methodsmap,filter,zip,enumerate,chain,take,skip,rev,fold,sum,count,min,max,any,all,find,nth,collectand more.core::option,core::result,core::cmp,core::clone,core::convert,core::panic.std::vec(Vec<T>:push,pop,get,set,len,iter),std::box,std::io,std::fs,std::tensor.
Three programs
Declarations, traits, enums and Option:
import core::option;
const SIDES : i64 = 4;
struct Pair<T> {
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<T> Pair<T> {
fn swap(self : Pair<T>) -> Pair<T> {
return Pair<T> {
first : self.second, second : self.first
};
}
}
fn largest<T : Describe>(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<i64> {
if a % 2 == 0 {
return Option<i64>::Some(a);
}
if b % 2 == 0 {
return Option<i64>::Some(b);
}
return Option<i64>::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<i64> {
first : 1, second : 2
}.swap();
let (q, rem) = divmod(17, 5);
println!("{} {} {} {}", p.first, p.second, q, rem);
match first_even(3, 8) {
Option<i64>::Some(v) => {
println!("even {}", v);
},
Option<i64>::None() => {
println!("none");
}
}
return 0;
}
It prints:
9 16 9
2 1 3 2
even 8
Tensors with a fixed shape and with a shape known only at run time:
// A shape known while compiling is checked while compiling.
fn dot<const N : i32>(a : Tensor<f32, [N]>, b : Tensor<f32, [N]>) -> 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<f32, [?, ?]>, b : &Tensor<f32, [?, ?]>) -> Tensor<f32, [?, ?]> {
let rows = a.extent(0);
let cols = b.extent(1);
let mut c : Tensor<f32, [?, ?]> = unsafe {
Tensor<f32, [?, ?]>::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<f32, [3]> = [1.0, 2.0, 3.0];
let b : Tensor<f32, [3]> = [4.0, 5.0, 6.0];
println!("dot {}", dot(a, b));
let m_flat : Tensor<f32, [4]> = [1.0, 2.0, 3.0, 4.0];
let id_flat : Tensor<f32, [4]> = [1.0, 0.0, 0.0, 1.0];
let m : Tensor<f32, [2, 2]> = m_flat.reshape([2, 2]);
let id : Tensor<f32, [2, 2]> = 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<i32, [2, 3]> = Tensor<i32, [2, 3]>::new();
let flat : Tensor<i32, [6]> = z.reshape([6]);
println!("zeros {}", flat[5]);
return 0;
}
It prints:
dot 32
1 2 3 4
zeros 0
Vec, iterators and Result:
import core::iter;
import core::option;
import core::result;
import std::vec;
fn parse_digit(c : i64) -> Result<i64, i64> {
if c >= 48 && c <= 57 {
return Result<i64, i64>::Ok(c - 48);
}
return Result<i64, i64>::Err(c);
}
fn main() -> i32 {
let mut v = Vec<i32>::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<i64, i64>::Ok(d) => {
println!("digit {}", d);
},
Result<i64, i64>::Err(c) => {
println!("not a digit: {}", c);
}
}
match v.pop() {
Option<i64>::Some(last) => {
println!("popped {}", last);
},
Option<i64>::None() => {
println!("empty");
}
}
return 0;
}
It prints:
len 5 total 55
odd squares 165
digit 7
popped 25