Skip to content

Coming from Rust

Every section below is a place where Lisette reads like Rust and behaves differently.

let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1);error: value moved
bindings.rs
let s1 = "hello"
let s2 = s1
fmt.Println(s1)ok
bindings.lis

Rust moves s1 into s2. Lisette has no moves, so s1 stays usable.

See Bindings

fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
greet.rs
fn greet(name: string) -> string {
f"Hello, {name}!"
}
greet.lis

String and &str collapse into one string. No owned versus borrowed distinction.

See String

let x = 42;
let r: &i32 = &x;
println!("{}", *r);
fn increment(r: &mut i32) {
*r += 1;
}
increment.rs
let x = 42
let r: Ref<int> = &x
fmt.Println(r.*)
fn increment(r: mut Ref<int>) {
r.* += 1
}
increment.lis

Ref<T> and mut Ref<T> line up with Rust’s &T and &mut T, but mut Ref<T> is not exclusive. &x yields whatever x allows, so a let mut binding gives a mut Ref<T> and a plain let gives a read-only one.

See References

let a = vec![1, 2, 3];
let mut b = a;
b[0] = 99;
println!("{:?}", a);error: borrow of moved value
alias.rs
let a = [1, 2, 3]
let mut b = aallowed, but b inherits a's permission
b[0] = 99error: b is read-only
let mut c = a.clone()independent storage
c[0] = 99write permitted, a untouched
alias.lis

A slice is a handle. Both names reach the same storage rather than a copy of it, and b inherits the read-only permission of a. .clone() severs the alias and hands back writable storage.

struct Index {
counts: HashMap<String, i32>,
tags: Vec<String>,
}
index.rs
struct Index {
counts: mut Map<string, int>,writable, through a writable Index
tags: Slice<string>,read-only
}
index.lis

Rust makes a whole value mutable at once, through &mut self or a mut binding. In Lisette, each field declares its own permission.

See Bindings, Structs and Safety

let mut total = 0;
let add = |n: i32| { total += n; };error: cannot borrow add as mutable
add(5);
closure.rs
let mut total = 0
let add = |n: int| { total += n }
add(5)
closure.lis

A closure captures by reference, and the garbage collector keeps what it captured alive. No borrow rules, and no move keyword.

See Lambdas

trait Display {
fn to_string(&self) -> String;
}
impl Display for Point {
fn to_string(&self) -> String {
format!("({}, {})", self.x, self.y)
}
}
display.rs
interface Display {
fn to_string() -> string
}
impl Point {Point satisfies Display
fn to_string(self) -> string {
f"({self.x}, {self.y})"
}
}
display.lis

In Lisette, a type satisfies an interface simply by having matching methods.

See Interfaces

#[derive(PartialEq)]
struct Order {
id: i32,
tags: Vec<String>,
}
equality.rs
#[equality]
struct Order {
pub id: int,
pub tags: Slice<string>,
}
equality.lis

Rust needs the derive before == works. Lisette accepts == without it, but only if every field is comparable. A slice, map or function field removes ==, and #[equality] supplies an equals() method in its place.

See Equality

let bytes = std::fs::read(path).unwrap();
read.rs
let Ok(bytes) = os.ReadFile(path) else {
fmt.Println("could not read")
return
}
read.lis

?, match, let else, and map_or work the same. Lisette has no unwrap().

See Failures