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, notuse.use core::option;isimport core::option;. An import brings in everything the module declares.- No
pub. Every top-level item is visible to a module that imports it. - No attributes. There is no
#[derive(..)],#[test]or#[inline].
Functions and methods
- The return type is always written.
fn log(x : i32)isfn log(x : i32) -> void, andmainreturnsi32:fn main() -> i32 { .. return 0; }. selfhas a type.fn get(&self)isfn get(self : &Self),&mut selfisself : &mut Self, andselfisself : Self. The type’s own name works too:self : &Point.- Closure parameters have types.
|x| x + 1is| x : i64 | x + 1. - No
impl Traitparameters.fn show(g : &impl Area)isfn show<T : Area>(g : &T).
Types and values
- No turbofish.
Vec::<i32>::new()isVec<i32>::new(). - An enum variant is written with its type.
Some(3)isOption<i64>::Some(3), andNoneisOption<i64>::None().Optioncomes fromimport core::option;. - An integer variable with no written type is
i32. Rust readslet mut total = 0;as ani64wheni64s are added to it later. Vx does not, so writelet mut total : i64 = 0;. - Arrays are tensors.
[i32; 4]isTensor<i32, [4]>. The shape is part of the type, and a?marks a size known only at run time:Tensor<f32, [?, ?]>. - Uninitialized storage is
unsafe.MaybeUninithas no equivalent. Writeunsafe { Tensor<f32, [4]>::uninit() }and set every element before reading it, or useTensor<f32, [4]>::new(), which is zeroed.
Control flow
- A
matcharm’s body is a block.0 => 7,is0 => { 7 },. - No
if letorwhile let. Usematch. - No
?operator. Usematchand return early.
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.- There is no function called
println:println(x)is an error. Writeprintln!("{}", 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<f32, [4, 4], Memory::NPU_HBM>. 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.unsafeallows raw pointers and calls tounsafe fn, as in Rust. It does not allow reading device memory from the host: that stays a compile error.
Topologies and memory explains these.
A complete program
Most of the differences above, in one program:
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<T : Area>(a : &T, b : &T) -> i64 {
return a.area() + b.area();
}
fn half(n : i64) -> Option<i64> {
if n % 2 == 0 {
return Option<i64>::Some(n / 2);
}
return Option<i64>::None();
}
// Rust would write `half(half(n)?)`.
fn quarter(n : i64) -> Option<i64> {
match half(n) {
Option<i64>::Some(h) => { return half(h); },
Option<i64>::None() => { return Option<i64>::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<i64>::Some(q) => { println!("quarter {}", q); },
Option<i64>::None() => { println!("no quarter"); }
}
let mut t : Tensor<f32, [3]> = unsafe { Tensor<f32, [3]>::uninit() };
for i in 0..3 {
t[i] = 0.5 * (i as f32);
}
println!("last {}", t[2]);
return 0;
}
It prints:
area 13
squares 14
quarter 3
last 1