Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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() -> 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<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 as name!(..).
  • extern "C" { fn abs(x : i32) -> i32; } declares a C function.

Types

  • Integers i8 i16 i32 i64, u8 u16 u32 u64; floats f16 f32 f64; bool. Nothing converts by itself: write x as i64. An integer variable with no written type is i32.
  • 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<f32, [2, 3]>: the shape is part of the type. ? is a size known only at run time, Tensor<f32, [?, ?]>, read with t.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> and Result<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 -> !, like panic("..").

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 ;.
  • 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<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, 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:

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 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<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