Kast for Rust programmers

Common programming concepts

Variables and mutability

let x: i32 = 5;
// x = 3; // compiler error
let mut x = "hi";
x = "yo";
let x :: int32 = 5;
# x = 3; # compiler error
let mut x = "hi";
x = "yo";
Current status:

mut is not respected currently

const C: u64 = 123;
comptime let C :: uint64 = 123;
Current status:

its actually just const C = 123 same as Rust for now because of technical issues

Shadowing works same as in rust:

let x: i32 = 0;
let x = x + 1;
{
    let x = x + 2;
    println!("{x}"); // prints 3
}
println!("{x}"); // prints 1
let x :: int32 = 0;
let x = x + 1;
(
    let x = x + 2;
    dbg x; # prints 3
);
dbg x; # prints 1

Data types

Same as Rust, Kast is a statically typed language, with similar inference.

let guess: u32 = "42".parse()
    .expect("Not a number!");
let guess :: uint32 = "42" |> parse;
# If parse fails it throws a 
# **checked** exception
# (it must be handled somewhere)

To help the inference (or assert the type) any expr/pattern can be type ascribed. Unlike Rust, type ascription can be used anywhere

let guess :: uint32 = ("42" :: &string)
    |> (parse :: &string -> uint32)
    :: uint32;

Kast is supposed to depend as little as possible from the target platform, giving you way to specify the types from the target like this: let int128 :: type = native "int128"; The most important target for Kast though is the interpreter, which allows you to evaluate the code during compile time. Common types are defined in the interpreter but their availability on target depends on specific compiler.

Same as Rust we have scalar types like integers, floating-point numbers, booleans, and characters. At the time of writing, we have these:

Default literal type

Unlike Rust, we don't have {integer}/{float} type for literals. We also don't have a default type for those literals (by default) - type needs to be specified (or inferred).

let x = 123; # compiler error: number literal type could not be inferred

It is also not required to use . for floating point numbers if the type is inferred: let x :: float64 = 123;. The behaviour of no default can be changed for some sections of code with compile time context (more on contexts later). Basically, this runs a function that returns the default literal type with the literal as a string, at compile time:

(
    comptime with (.default_number_type = _ => std.int32) :: std.default_number_type;
    dbg 123; # inferred as int32
);
(
    comptime with (.default_number_type = _ => std.int64) :: std.default_number_type;
    dbg 123; # inferred as float64
);
(
    # mimic Rust behavior
    comptime with (
        .default_number_type = s => (
            if std.contains (.s, .substring=".") then
                std.float64
            else
                std.int32
        ),
    ) :: std.default_number_type;
    dbg 123; # inferred as int32
    dbg 123.0; # inferred as float64
);

if false then (
    # no default - this is going to fail to compile
    std.dbg 123;
);

TODO: not implemented yet, but you also can have number literals be treated as custom types: let x :: BigInt = 1238762345761576453124617235476124;

TODO: hex/octal/binary literals not supported yet

Overflow behavior

Overflow behaviour for integers is also working through the context system - behavior depends on what is currently chosen. By default it is panicking on overflow, but you can change it:

with saturating (
  a + with overflowing (b + c)
)

# for compiler to optimize the checks away
with undefined_behavior_on_overflow (
  a + b
)
Current status:

Not implemented yet

Tuples/structs

In Kast, tuples can have both unnamed and named fields at the same time. Then, custom structs are just newtyped anonymous tuples.

type A = (i32, String)
struct B(i32, i32);
struct C { x: f64, y: f64 }
let A = (int32, string);
let B = newtype (int32, int32);
let C = newtype (.x = float64, .y = float64);

let ImpossibleInRust = (int32, float64, .named = string);

As we will see soon, all functions have just a single argument and this allow to have functions with unnamed/named arguments

TODO: also have optional/repeated/kwargs

Lists

For now Kast just has lists as alternative to Rust's Vec. Lists will most likely be changed.

let mut x: Vec<i32> = vec![1, 2, 3];
x.push(123);
dbg!(x.len());
let x :: list[int32] = list[1, 2, 3];
list_push (&x, 123);
dbg (list_length &x);

Strings

For now Kast only ha a single type for strings. But, the string literals can act both as strings and references to strings (depends on inference, defaults to owned string).

let x = "hello"; // x: &str
let x = String::new("world"); // x: String
let x :: &string = "hello";
let x :: string = "world";

Functions

As said before, all functions is Kast have a single argument. If you want multiple arguments, you use a tuple as argument. Also, all functions work as closures in Kast.

type F = fn(i32) -> String;
type G = fn(i32, f64) -> bool;

let foo: fn(i32, i32) -> i64 = |x, y| x + y;
fn goo(x: i32, y: i32) -> i32 {
    x + y
}
let F = int32 -> string;
let G = (int32, float64) -> bool;

let foo :: (i32, i32) -> i64 = (x, y) => x + y;
let goo = fn(x :: int32, y :: int32) {
    x + y
};

Unlike rust, Kast supports recursive closures, but it needs to be declared in a recursive scope. All the bindings that are introduced in a recursive scope become the fields of the resulting struct.

let rec_scope = rec (
  let f = depth => (
    if depth < 3 then (
      print "inside f";
      dbg depth;
      g (depth + 1);
    );
  );
  let g = depth => (
    print "inside g";
    dbg depth;
    f (depth + 1);
  );
);
rec_scope.f (0 :: int32)

Calling a function does not require parentheses: std.print "hello". You can also use pipe operator: "world" |> std.print.

Context system

Another key feature for Kast in the context system. It is similar to effect systems/capabilities/implicit arguments.

Examples of contexts would be:

Basically, functions in Kast are also having context types as another part of function type specification - std.print :: &string -> () with output. This says that std.print needs access to output in order to be able to be called.

When calling a function, it is required that the context is available. Otherwise there will be a compilation error.

Contexts can be of any type, and you can introduce a context by providing a value:

with (
    .write :: &string -> () = text => launch_the_rocket_with_message text,
) :: output;

std.print "hello, world";

In this case std.print will not write to the stdout but instead launch a rocket.

Current status:

Inference needs to be improved

As we've seen earlier, contexts may also be introduced at comptime. This can be used to change some compiler behavior.

Mutability with contexts

Mutability in Kast is also done with the context system - if a function needs to mutate a variable it means that it requires mutable access to the variable.

let mut x = 0;
let inc = () => x += 1;
let dec = () => x -= 1;
inc(); inc(); dec();
dbg x; # prints 1

This example doesn't compile in Rust but it does in Kast. Both functions here only capture the pointer to x, without capturing the access. Instead, access is going to be required when calling these functions. The full type of inc and dec is () -> () with mutable_access[x]. Mutable access context is introduced automatically when declaring a mutable variable.

Current status:

Mutability with context system is not implemented yet

Lifetimes are also planned

Comments

Control flow

Generics

Generics in Kast are implemented as just functions. A generic type is just a function that takes a type and returns a new type. A generic function is a function that thats a type and returns a function.

struct Foo<T> { field: T }
let foo: Foo<i32> = Foo { field: 123 };
fn id<T>(x: T) { x }

// explicit generic arg with turbofish
let x = id::<i32>(123);
// generic arg inferred based on result
let x: i32 = id::<_>(123);
// generic auto instantiation
let x: i32 = id(123);
let Foo :: type -> type = T => (.field = T);
let foo :: Foo int32 = ( .field = 123 );
let id = (T :: type) => ((x :: T) => x);

# calling a "generic" function
let x = id(int32)(123);

Actually, since we want the generic argument to be inferred sometimes, and not be written explicitly Kast does have a dedicated type for it - called templates.

Templates are just functions, but calling and defining them needs a different syntax. We can also omit explicitly calling them and use auto instantiation. But otherwise they are still just functions.

let Foo = forall[T :: type] { .field = T };

let foo :: Foo[int32] = ( .field = 123 );
# use _ to infer the argument
let foo :: Foo[_] = ( .field = 123 :: int32 );

let id = forall[T] { (x :: T) => x };
# explicit arg
let x = id[int32](123);
// arg inferred based on result
let x :: int32 = id[_](123);
// auto instantiation
let x :: int32 = id(123);

Traits

struct Foo { a: i32, b: i32 }
trait Clone {
    fn clone(&self) -> Self;
}
impl Clone for Foo {
    fn clone(&self) {
        (self.a, self.b)
    }
}
fn duplicate<T: Clone>(x: T) -> (T, T) {
    (
        <T as Clone>::clone(&x),
        <_ as Clone>::clone(&x),
    )
}
let foo: (Foo, Foo) = duplicate(Foo { a: 1, b: 2 });
const Foo :: type = ( .a = int32, .b = int32);
const Clone = forall[Self] {
    .clone = &Self -> Self,
};
impl Foo as Clone = (
    .clone = self => ( .a = (self^).a, .b = (self^).b),
);
let duplicate = forall[T] {
    fn (x :: T) -> (T, T) {
        (
            (T as Clone).clone(&x),
            (_ as Clone).clone(&x),
        )
    }
};
let foo :: (Foo, Foo) = duplicate ( .a = 1, .b = 2 );

In Kast, trait impls are just normal values, and traits are just types (generic types). Since generic types are functions returning types, implementing a trait for a type means providing the value with type equal to applying that function (template) with argument being the type for which you implement the trait.

T as Trait :: Trait[T] is an expression that retieves the implementation. impl T as Trait = Impl :: Trait[T] is how you implement a trait.

Current status:

Can only implement for concrete types - no generic implementations yet

Trait bounds are not implemented yet

Macros

macro_rules! my_macro {
    ($e:expr) => (
        let x = $e;
        dbg!(x);
    )
}
my_macro!(2 + 2);
const my_macro = macro (e) => `(
    let x = $e; dbg x
);
my_macro!(2 + 2 :: int32);

Macros in Kast are also almost normal functions, but they operate on ASTs. In the above example my_macro has type macro ast -> ast, so its a function that takes ast and returns ast.

`(some code) is the quoting operator, similar to quote! macro from the quote crate in rust - it parses the code and produces the ast. Inside the quote you can use the unquote operator $ to interpolate expressions.

Kast macro system can be used for extending systax.

syntax ternary -> 13.1 = condition "?" then_case ":" else_case;
impl syntax ternary = macro (.condition, .then_case, .else_case) => `(
    if $condition then $then_case else $else_case
);

let x :: int32 = true ? 1 : 0;
Current status:

ast pattern matching is not implemented yet