Welcome

This book acts as the specification for the Lazy programming language! Every detail about the language (grammar, data types, memory management, JIT) is provided, as well as examples.

Lazy is a statically typed, interpreted programming language.

A taste

struct Person {
    name: str,
    age: f32,
    hobby: str?,

    age_up: fn() {
        self.age += 1;
    }

}

enum AnimalTypes {
    Dog,
    Cat,
    Fish,
    Bird
}

struct Animal {
    name: str,
    age: i8,
    type: AnimalTypes
}

type ThingWithName = {name: str};

main {
    let me = new Person { name: "Google", age: 19 };
    let my_pet = new Animal { name: "Baby", age: 1, type: AnimalTypes::Bird };

    let things = new Vec<ThingWithName>{};
    things.push(me);
    things.push(my_pet);

    for thing in things {
        print(thing.name);
    }
}

Primitives

Much like the rest programming languages, Lazy provides a bunch of primitive data types.

Scalar types

  • Signed integers: i8, i16, i32
  • Unsigned integers: u8, u16, u32
  • Floating points: f32
  • Characters: char
  • Strings: str
  • Booleans: true or false
  • The none data type

Compound types

Tuples

Tuples can hold values of different types:

const items<[str, i32, bool]> = ["Hello", 64, true];

items.1; // 64

Arrays?

Lazy does not support arrays natively, but it does provide a Vec struct, which is an array located on the heap. Those are always more useful anyways.

let nums = Vec::from(1..=10);
nums.push(15);
nums.filter(fn(n) n % 2);

Literals

Almost all literals are what'd you expect them to be.

Numbers

let my_age = 18; //  inferred type: i32
let my_height = 6.1; // inferred type: f32

let not_inferred: u8 = 12; // Not inferred, number is an u8

Numbers can contain underscores (_) to improve readability:

let big_num = 100_00_00; 
let same_big_num = 1000000;
big_num == same_big_num; // returns "true"

To improve working with milliseconds, numbers can be prefixed with s, m, h and d to be automatically converted. This is done during compile time, so there's zero overhead!

let one_min_in_milliseconds = 1m; 
one_min_in_milliseconds == 60000; // returns "true"

Binary / Octal / Hex literals

Numbers can be prefixed with 0b, 0o and 0x to be parsed as binary / octal / hex numbers.

0b010101 == 21 // true
0o03621623 == 992147 // true
0x0123ABCd == 19114957 // true

Template literals

String concatenation is very messy in many languages - Lazy makes this easy with template literals:

let age = 1 + 17;
print(`Hello World! I am ${age} years old`); // Hello World! I am 18 years old

They work exactly like in javascript!

Iterators

0..5; // Creates an iterator from 0 (inclusive) to 5 (exclusive), so 0, 1, 2, 3 and 4.
5..=10; // Creates an iterator from 5 (inclusive) to 10 (inclusive) 
..5; // Short syntax for 0..5
..=10; // Short syntax for 0..=10

Functions

In Lazy, functions are first-class citizens, you can pass them as parameters, save them in variables and so on.

let my_fn = fn() 1 + 1;
my_fn(); // returns 2

Natural literals

natural literals are tuples or iterators which can only contain literals. For example:

person.age..=18; // This is NOT a natural iterator
0..=18; // This IS, because both the start and the end are literals

[1, 2, none, true] // This IS a natural tuple
[1, 2, func(1 + 1)] // This is NOT a natural tuple

Only natural tuples or iterators are allowed in match arms.

Functions

In Lazy, functions are first-class citizens, this means they can be passed as function arguments, stored easily in data structures, and be saved in variables.

main {
    const say_alphabet_lowercase = fn () print(...'a'..='z');
    say_alphabet_lowercase(); 
}

A function may have up to 255 arguments!

Function return values

A return value type can be specified to any function by adding and arrow and the type after the arguments part:

const is_ok = fn() -> bool {
    true;
}

As you notice, there's no return statement in Lazy. The last expression in the function body always gets returned. Everything in Lazy is an expression, except type, struct and enum declarations, so it's all good.

If a return type is not provided, the function will always return none.

Optional parameters

const add(num1: i32, num2: i32, num3: i32?) -> i32 {
    num1 + num2 + (num3 || 0);
}

Default values

const add(num1: i32, num2: i32, num3 = 0) -> i32 {
    num1 + num2 + num3;
}

Execution context

All functions in Lazy have an execution context which can be referenced via self in the function body. The execution context of functions is always none, unless the function is defined inside a structure:

struct Car {
    model: str,
    beep_noise: str,

    beep: fn() {
        print(self.beep_noise); 
    }
}

Iterators

Iterator is a type which allows you to loop over a struct. for...in loops are powered by iterators. An Iterator is any struct which satisfies the following type:

type Iterator<T> = { next: () -> T? }

Any struct with a "next" function is a valid iterator!

struct RangeIter {
    min: i32
    max: i32
    progress: i32

    next: () -> i32? {
        if self.progress == self.max none 
        else self.progress += 1
    }
}

main {
    let my_range = new Range { min: 0, max: 15, progress: 0 };
    for i in my_range print(i);
}

Literals

Iterator literals are called range iterators, because they create an iterator that iterates through a range of numbers.

0..5 // Exclusive
0..=5 // Inclusive

Spreading iterators

The spread syntax can be used to turn iterators into vectors!

let iterator = 0..10;
let vec = ...iterator;
print(vec.to_string()); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

Spread syntax in functions

The spread syntax has a special meaning in function parameters:

type Stringable = { to_string: () -> str };

const print = fn(...strs: Stringable?) {
    // code
}

The ...strs: syntax means that the function can receive an unlimited amount of values with the Stringable type. The strs value itself is either a Vector which contains Stringable, or none..

const println = fn(...strs: Stringable?) {
    for string in strs {
        print(string?.to_string() + "\n");
    }
}

print_many("Hello", new Vec{}, 4, true); 
// Prints:
/*
Hello
[]
4,
true
*/

Custom Types

There are two ways to create custom types in Lazy:

  • enum - For creating enumerations with different variants, which can hold values of different types
  • struct - Structs are very similar to C structs, except they can contain functions

Type aliasing

Type aliasing can be done by using the type keyword:

type i = i8;

main {
    let a: i = 4;
}

None-able types

A type which is prefixed with ? is none-able. The value it may have may be none or the type.

static div = fn(a: i32, b: i32) -> i32? {
    if b == 0 none
    else a / b
}

Enums

Enums are types which may be one or a few different variants.

enum Number {
    Int: i32
    Float: f32
}

enum TokenTypes {
    String: str
    Integer: i32
    Number: Number
    Character: char
    Invalid
}

main {
    let str_token = TokenTypes::String("some token type");
    str_token == TokenTypes::String; // returns true

    // str_token gets automatically unwrapped
    if str_token == TokenTypes::String print(str_token) // prints "some token type"
    else if str_token == TokenTypes::Integer print(str_token + 5)

    let num_token = TokenTypess::Number(Numbers::Int(3000));
}

"Unwrapping" enums

By default all items are "wrapped" - their values and unknown and can be anything.

let char_token = TokenTypes::Char('a'); 
// Even if it's clear that char_token is of type `char`, Lazy doesn't allow you to use it. 

To use the value inside the variant you'll have to "unwrap" it.

if unwrapping

if let TokenTypes::Char(ch) = char_token {
    // You can use `char_token` as a character now.
    ch.to_string(); // "a"
}
// char_token is wrapped here.

if char_token == TokenTypes::Char('a') {
    // char_token is still wrapped, but you know it's value.
}

match unwrapping

match char_token {
    TokenTypes::Char(ch) => {
        print(ch);
        // char_token is unwrapped here
    },
    _ => {
        // char_token is wrapped, this portion only executes when nothing else got matched
    }
}

Attaching methods and properties to enums

Attaching methods and properties to enums is done via the impl keyword.

enum Option<T> {
    Some: T,
    None
}

type Unwrap<T> = {
    unwrap: () -> T,
    is_some: () -> bool,
    unwrap_or: (v: T) -> T
}

// The impl block's typings must match the structure's
impl<T> Unwrap<T> for Option<T> {
    unwrap: fn() -> T {
        match self {
            Option::Some(val) => val,
            Option::None => error("Tried to unwrap an empty option!")
        }
    }

    is_some: fn() -> bool {
        self == Option::Some;
    }

    unwrap_or: fn(v: T) -> T {
        match self {
            Option::Some(val) => val,
            Option::None => v
        }
    }
}

main {
    let maybe: Option<i32> = Option::None;
    maybe.unwrap_or(0); // 0
    maybe = Option::Some(32);
    maybe.is_some(); // true
    maybe.unwrap(); // 32
}

Structs

Structs contain key-value pairs called fields. All keys and the types of the values must be known at compile-time.

struct Something<GenericParam> {
    val: GenericParam,

    set: fn(val: GenericParam) -> bool {
        self.val = val;
        true;
    }

}

Field modifiers

All of the following field modifiers can be used together on a single field, and the order of the modifiers does not matter.

Static fields

Sometimes you want a field to be attached to the struct itself and not an instance of the struct.

struct Person {
    name: str,

    static create: fn(name: str) -> Person {
        new Person { name };
    }

}

let me = Person::create("Google");

Hidden/Private fields

Fields that can only be accessed in the execution context of the functions inside the sctruct.

struct Person {
    name: str,
    private id: i32

    static create: fn(name: str) -> Person {
        new Person { name, id: rng() };
    }

}

let me = Person::create("Google");
me.id; // Error!

Immutable fields

Fields that cannot be mutated.

struct Person {
    const name: str,
    private id: i32

    static create: fn(name: str) -> Person {
        new Person { name, id: rng() };
    }

}

let me = Person::create("Google");
me.name = "Pedro"; // Error!

Optional fields

Fields which have a question mark (?) after the type are optional.

struct StructWithOptionalField {
    // Can either be "none", or "str"
    something: str?
}

Accessing fields

Fields are accessed using the dot notation (.).

main {
    let my_smth = new Something<str>{val: "Hello"};
    my_smth.set(val: "World");
    my_smth.val.length; // returns 5
}

Accessing optional fields

Optional fields can be accessed by the dot notation, but you must put a question mark (?) before the dot. If the optional struct is empty, then the expression returns none.

main {
    let my_struct = new StructWithOptionalField{}; // the optional field is `none`
    my_fn(my_struct.something?); // The function doesn't get executed because my_struct cannot be unwrapped
    my_struct.something = "Hello";
    my_fn(my_struct.something?); // Runs fine!
}

Keep in mind that you cannot use optional fields before you make sure they are not none.

if my_struct != none {
    // my_struct is guaranteed to not be none here, no need for question marks!
    print(my_struct.something);
}

Operator overloading

Operator overloading is done via partials.

import * from "std/ops" as Ops

struct Person {
    first_name: str
    middle_name: str
    last_name: str
}

impl Ops::Add<Person, str> for Person {

    add: fn(other: Person) -> str {
        self.first_name + " " + other.middle_name + " " + other.last_name;
    }

}

main {
    const me = new Person {first_name: "Google", middle_name: "Something", last_name: "Feud"};
    const you = new Person {first_name: "Taylor", middle_name: "Alison", last_name: "Swift"};
    print(me + you); // Google Alison Swift
}

Partials

Lazy does not have core OOP design patters like inheritance, but it has Partials, which are very similar to rust's Traits, but a little more dynamic.

Partial struct

struct Animal {
    name: str,
    age: i8,

    make_noise: fn(noise: str?) print(noise || "*quiet*")
}

struct Human {
    name: str,
    age: i8,
    job: str

    make_noise: fn() print("Hello World")
}

type WithName = { name: str }

main {
    // The function will only have access to the name field
    let get_name = fn(thing: WithName) -> str {
        thing.name;
    }

    let me = new Human{name: "Google", job: "Programmer", age: 19};
    let some_animal = new Animal{name: "Perry", age: 2};

    get_name(me); // Google
    get_name(some_animal); // Perry
}

Combining types

Partials can be combined to create more complex partials:

type Stringable = {
    to_string: () -> str
}

type Numberable = {
    to_int: () -> i32
}

static num_and_str = fn(val: Stringable + Numerable) -> [str, i32] {
    [val.to_string(), val.to_int()];
}

Partial function

// Requires the type to have a "make_noise" method - we don't care about params or return value in this case
type MakesNoise = { make_noise: () };

main {
    let me = new Human{name: "Google", job: "Programmer", age: 19};
    let some_animal = new Animal{name: "Perry", age: 2};

    let stuff_that_makes_noise = Vec::create<MakesNoise>();
    stuff_that_makes_noise.push(me);
    stuff_that_makes_noise.push(some_animal);

    stuff_that_makes_noise[0]?.make_noise(); // "Hello World"
    stuff_that_makes_noise[1]?.make_noise(); // *quiet*
}

Impl

The impl keyword is used to combine a struct or an enum with a partial type, so the struct/enum fits the partial. Generally, this keyword doesn't have to be used with structs in order to make a struct compatible with a type, but it's nice syntactic sugar.

struct Human {
    age: i8
}

struct Animal {
    age: i8,
    species: AnimalTypes
}

type Speak = { speak: (...args: str?) -> str } // A partial which requires the structure to have a "speak" method which returns a `str`

impl Speak for Human {
    speak: fn(...args: str?) -> str {
        if args.0 args.0;
    }
}

impl Speak for Animal {
    speak: fn(...args: str?) -> str {
        match self.species {
            AnimalTypes::Cat => "meow",
            AnimalTypes::Dog => "woof",
            AnimalTypes::Mouse => "*squick*"
        }
    }
}

Type guards

This feature can also be used to create type guards:

type TotalStrLen {
    total_len: () -> i32
}

impl TotalStrLen for Vec<str> {
    total_len: fn() -> i32 {
        let len = 0;
        for string in self {
            len += string.length;
        }
        len;
    }
}

main {
    let vec_of_strs = Vec::from<str>("a", "b", "cde");
    vec_of_strs.total_len(); // 5
    let vec_of_nums = Vec::from<i32>(1, 2, 3, 4, 5);
    vec_of_nums.total_len(); // Error!
}

Generic bounds

In structs, enums and types, generic parameters can be bounded. That means that only a type which satisfies the specified partial can be passed as a generic parameter.

struct Error<
    T: { to_string: () -> str }
> {
    content: T,
    type: i32,
    format: fn() -> str {
        `Error ${self.type}: ${self.content}`;
    }

}

struct CustomErrorContent {
    vars: Vec<str>,
    to_string: fn() -> str {
        vars.join(", ");
    }
}

main {
    const my_custom_error_content = new CustomErrorContent { vars: Vec::from("Hello", " ", "World!") };
    const error = new Error<CustomErrorContent> { content: my_custom_error_content, type: 1 };
    print(error.format()); // "Error 1: Hello World"
}

Variables

Defining variables is done with the let, const and static keywords.

let

let defines a mutable variable. The value inside the variable itself is always mutable, but the variable can be changed.

let a = 5;
a = 10; // Valid
a = 3.14; // Invalid, `a` is implicitly an i32

const

const defines an immutable variable. The value is still mutable, but the variable cannot be changed.

const a = 5;
a = 10; // Invalid
struct Pair<V> {
    key: str,
    val: V
}

const my_pair = new Pair<i32>{key: "Hello", val: 3};
my_pair.key = "World"; // Valid

static

static is used for values you never want to get garbage-collected. They cannot be defined in functions or the main block.

static PI = 3.14;

main {
    PI = 4; // Invalid
    let PI = 4; // Invalid
    const PI = 5; // Invalid
}

Type hinting in variables

let someOption = none;  // Incorrect

Here, the compiler doesn't know what else can someOption be other than none, so it throws an error. You need to specify the other possible type.

let someOption: i32? = none; // Correct!

Deconstructing structs or tuples

let my_tuple = [1, 2, 3, 4, 5];
let [firstElement, secondElement] = my_tuple;
print(firstElement, secondElement); // 1, 2
struct Student {
    grades: Map<str, Grade>
    favorite_subject: str
}

const { favorite_subject } = new Student { grades: Map::create(), favorite_subject: "programming" };
print(favorite_subject); // "programming"

Logic

if/else

In Lazy, if/else conditionals are expressions. All branches of the condition must return the same type, or none. Any expression can be used for the condition and the body - even other if expressions.

const a = 10;

if a > 10 print("a is more than 10")
else if a > 5 print("a is more than 5")
else print("a is less than 5")

enum Number {
    Int: i32,
    Float: f32
}

let enum_field = Number::Int(15);

// if "enum_field" is of type `Number::Float`, return the unwrapped float inside it, otherwise return 0.
let my_num = if let Number::Float(num) = enum_field enum_field else 0;

print(my_num == 0) // true

match

The match expression is exactly the same as Rust's match.

let my_num = Number:Float(3.14);

match my_num {
    Number::Float(num) => print("Found float ", num),
    Number::Int(num) => print("Found integer ", num),

    // Specific cases
    Number::Float(3.14) => print("Found pi!"),

    // Guards
    Number::Int(num) if num > 10 => print("Found integer bigger than 10"),

    // Execute the same body for different expressions
    Number::Int(3) | Number::Int(5) => print("Number is either 3 or 5"),

    // Acts as an "else" - only executed when nothing above gets matched
    _ => print("Weird...")
}

The contents of the match expression are called arms - each arm has a condition (which can be followed by a guard) and a body.

A condition can be:

  • Enum variants (Enum::variant)
  • Literals ("hello", 'c', 3, 45.3, [1, 2, 3], true, false, none)
  • Range iterators (0..10, 5..=1000, 'a'..='z')
  • A list of expressions separated by | (1 | 5 | 7) - if not all expressions return the same type, the value doesn't get unwrapped, but the body still gets executed.

Guards can be any expression.

A body can be any expression.

Loops

Loops are an imperative way to execute an expression multiple times. Lazy provides two way to create an imperative loop: for and while.

For

In Lazy, the for loop only has one variant - for...in:

for i in 0..10 {
    print(i);
}
// Prints 0, then 1, then 2... up to 9

The expression after in can be any valid iterator - and an iterator satisfies the following type:

type Iterator<T> = { next: () -> T? } // Any struct with a "next" function is a valid iterator!

While

A traditional while loop. The expression gets executed as long as the condition is true.

while true {
    // A never ending loop!
}

Breaking a loop

Both types of loops are expressions... but what do they return? By default, none, unless you use the yield keyword inside them. The yield keyword stops the execution of the loop and returns the provided expression.

The following snippet checks if 20 is in vector and if it is, the value variable is set to true, otherwise it's none.

let vector = new Vec<i32>{ iter: 0..30 };

let value = for i in vector {
    if i == 20 yield true
}

if value print("20 is in the vector!")
else print("20 is NOT in the vector!")

Promises

Async programming in Lazy is made to be simple and easy:

import "std/http" as Http

main {
    const res = await? Http::get("https://google.com");
    print(res.body);
}

Creating promises

let prom = Promise::spawn(fn() -> Result<i32, none> Result::Ok(1 + 1));
let res = await? prom; // 2

Promises return a Result, which is a special enum with two possible variants - T, the actual return value of the promise, or an Error:

Promise::spawn(fn() -> Result<none, str> {
    Result::Error("This promise always returns an error :x");
});

A question mark (?) can be put after the await keyword to short-circuit the execution context if the promise returns an Error.

Under the hood

Under the hood, Promises are handled by the tokio runtime.

Timers

Repeating a function

let counter = 0;
Promise::interval(fn() {
    print(counter);
    counter += 1;
}, 1m);

Timeout

Promise::timeout(fn() {
    print("This will be executed in 5 seconds!");
}, 5s);

Stopping function execution

await Promise::block(5m);

Modules

Sometimes you want to split your code to multiple files - modules are just the thing you need!

Exporting

You can export the following things:

  • struct declarations
  • enum declarations
  • static variables
  • type aliases
export enum SongGenre {
    Rock,
    Pop,
    Metal,
    Jazz,
    Rap,
    HipHop
}

export struct Song {
    const lyrics: str,
    const genre: SongGenre
}

export static create_song = fn(lyrics: str, genre: SongGenre) -> Song {
    new Song { lyrics, genre } 
}

Importing

import { create_song } from "./path/to/songs"

main {
    let bloodmoney = create_song("what do you believe in?", SongGenre::Metal);
}

as binding

import * from "./path/to/songs" as Songs;
import { create_song as create_a_song } from "./path/to/songs";
 
main {
    let bloodmoney = Songs::create_song("what do you believe in?", Songs::SongGenre::Metal);
}

Metaprogramming

Lazy allows you to create function macros which allow you to write cleaner and less-repetitive code. They're very similar to rust macros:

macro my_macro($a: ident, $b: expr) => {
    $a[$b];
}

main {

}