Introduction

Rock is an experimental native language.

Its main goal is to mix some parts of popular functionnal languages like haskell or livescript with the rigor of Rust while staying elegant and fast with minimal runtime

Rock is at an early development stage. Don't expect everything to work smoothly. (you have been warned)

struct Player
  level: Int64
  name: String

impl Player
  new: x ->
    Player
      level: x
      name: "MyName"

impl Show Player
  @show: -> @name + "(" + @level.show! + ")"

use std::print::printl

impl Print Player
  @print: -> printl @

main: ->
  let player = Player::new 42
  player.print!

Rock syntax is entierly based on indentation, like Livescript.
Each whitespace count, and tabulations \t are prohibited.

The number of whitespace to make one level of indentation is taken from the first indent level.
If your first indentation has two whitespaces, the rest of the file must have the same number of whitespace per level (here two)

We generally use two as a default, but you can use any number you want. Here is the same example with four whitespaces:

struct Player
    level: Int64
    name: String

impl Player
    new: x ->
        Player
            level: x
            name: "MyName"

Quick start

The actual best way to start is to build and install Rock via cargo:

cargo install --git https://github.com/Champii/Rock --locked
rock -V

Then to create a new empty project folder

rock new my_project
cd my_project

The project folder should contain a src folder with a src/main.rk file that should look like this:

main: -> "Hello World !".print!

You can immediately build and run this default snippet with

rock run

This should output

Hello World !

The compiler has created a build folder containing your compiled executable build/a.out

Language reference

Primitives

Rock actually support some essential primitives.

Boolean

The Bool type

main: -> 
  true.print!
  false.print!

Integer

The most stable integer is Int64 and should be used until the rest follows.

main: -> 0

Rock has an inference mechanism that automatically assign types to variables and primitives

Here the main function is special and has a fixed signature (Int64) so 0 is a Int64
You can learn more about Function signature

Float

main: ->
  let x = 2.2
  x.print!

Char

main: -> '*'.print!

String

Strings are immutable (like in most language)

main: -> ("Hello" + " World!").print!

You can index them to get a Char

second_char: -> "Hello"[1]

Strings are 0-indexed

Both Strings and Chars accept some escaped characters: \\ \' \" \n \r \0

In the future there will be a distinction between native Str and on-the-heap String like in Rust

Array

There is very limited support for arrays, they are still in development.

main: ->
  let a = [1, 2, 3]
  a[2] = 4
  a[2]

Function

Syntax

The function declaration format is:

function_name: arg1, arg2, arg3 -> return_value

The function call format is:

function_name arg1, arg2, arg3

But you can add parenthesis

function_name(arg1, arg2, arg3)

Every Rock package must have a ./src/main.rk file containing a main function

main: -> "Hello World!".print!

You can call functions with no args with a bang ! like the .print! above or with explicit parenthesis like .print()
But the idiomatic way is to avoid parenthesis as much as possible

add: x, y -> x + y
main: -> add 2, 3

Rock does not allow for uppercase identifiers, so you should embrace the snake case. Uppercase names are reserved for custom types like Struct or Trait

Polymorphism

Every function in Rock is polymorphic by default, and only infer the types based on the caller and the return value of its body. Multiple calls with different types will generate each corresponding function, just like the templates in C++ or in Rust, except the generic parameter is always implicit if no constraints have been made.

For example, lets declare the most polymorphic function of all, id:

id: x -> x

This function takes a x argument and returns it. Here x can be of any type

main: ->
  id 42
  id 6.66
  id "Hello"
  id my_custom_struct

The infered signature of the function is id: a => a, with a being any type

If we had changed the body of id to be:

id: x -> x + x

the previous main would still work if all of the types implemented the Num trait from the stdlib, that provide implementation of + for the basic types

Function signature

Syntax

foo: a => Int64 => String
foo: x, y -> "bar"

Here, the foo function takes 2 arguments, of type a (generic) and Int64, and returns a String The implementation ignore the two arguments and returns "bar"

A signature is always formed of at least one type.
The last (or only) type is the return type

Functions can take functions as parameter, and must be representable in a signature:

my_func: a => (a => b) => b
my_func: x, f -> f x

# A function of type (a => b) that resolves to (Int64 => String)
handler: x -> x.show!

main: -> my_func 42, handler .print!

Here the second argument of the function my_func is a function that take a generic type a and returns a type b, and the whole method returns a type b

This outputs:

42

Structure

Declaration:

Here is the idiomatic way to declare a structure

struct Counter
  value: Int64
  name: String

This structure has two fields value and name with their types respectively Int64 and String

Implementation

You can attach some methods to the structure, for example here a class-method new that takes a x and is used as a constructor

The increment method is an instance-method that takes nothing, increments the value field by 1 and returns @

impl Counter
  new: x ->
    Counter 
      value: x
      name: "Counter"

  @increment: @->
    @value = @value + 1

You can learn more about the @ parameter here: Self

main: ->
  Counter::new(41)
    .increment!
    .value
    .print!

This prints 42


*Note: We could have written something more compact, but less readable

impl Counter
  new: x -> Counter value: x, name: "Counter"
  @increment: @-> @value = @value + 1

main: -> Counter::new 41 .increment!.value.print!

Trait

Declaration

The trait feature is very similar to what Rust have
You can define some 'interfaces' that has some methods, you can then implement them for your types.

For example, we will implement a trait that double itself with the + operator

trait CanDouble
  @double_me: @

We define a trait CanDouble and declare a method double_me that returns the generic self type @.

Implementation

We can now implement the trait for any type we want, like Int64

impl CanDouble Int64
  @double_me: -> @ + @

We can then call this method:

main: -> (2).double_me!.print!

This output

4

Default method

You can define trait methods that have a default implementation.
That means you don't have to reimplement it for each type, but you can override the default implementation with your own if you need it.

trait CanDouble
  @double_me: -> @ + @

impl CanDouble Int64

main: -> (2).double_me!.print!

Overriding

You can override a default implementation

trait CanDouble
  @double_me: -> @ + @

impl CanDouble Int64
  @double_me: -> @ * 2

main: -> (2).double_me!.print!

Self

The self parameter

The self parameter is represented by the @ symbol.

impl CanDouble Int64
  @double_me: -> @ + @

This method desugars to:

impl CanDouble Int64
  double_me: self -> self + self

Those are strictly equivalent, as @ desugar to self We can see that we also have a @ at the start of the name of the method. This allows for auto-injection of the self-parameter:

Auto-inject self parameter

The standard way of defining a self-method is to auto-inject the self parameter:

impl CanDouble Int64
  @double_me: -> @ + @

Auto-return self

And if you also wanted to return self for chainable capababilities:

impl CanDouble Int64
  @double_me: @->
    @ = @ + @

main: ->
  let x = 2
  x.double_me!.double_me!.double_me!
  x.print!

Outputs

16

The @-> automatically inject the @ as last returned statement of the function's body. This operator is only available when using the @method_name: notation