The Concrete Programming Language

This book is a work in progress, much like the language itself, proceed with caution.

Some stuff may be outdated.

Getting Started

Here we will discuss:

  • Installing Concrete on Linux, macOS.
  • Writing a simple program.
  • Using concrete to create a project.

Installation

Currently Concrete is distrubted via source only, so you will have to compile it:

git clone https://github.com/lambdaclass/concrete.git cd concrete make build cp ./target/release/concrete /usr/local/bin/

Creating a project

To create a project using concrete you can use the new subcommand:

concrete new <project_name> [--lib]

You can pass --lib to make it a library.

To build the project simply go into the created dir and run concrete build

The Language

Concrete is currently in a very early development phase, we are first setting the bases of the compiler, to this aid, some syntax or features of the language may not reflect or fit the final view of "Concrete" we have.

Be warned many features are incomplete and the language should not be used for anything serious yet.

Modules

A module contains functions, structs, methods and types:

#![allow(unused)] fn main() { mod modulename { // .. } }

Currently there needs to be a top level module in every file.

Variables

You can declare a variable the following way:

#![allow(unused)] fn main() { let x: i32 = 2; }

Currently you always have to specify the type on the left hand side.

Functions

A function can be defined the following way:

#![allow(unused)] fn main() { pub fn name(arg1: i32) -> i32 { return arg1 * 2; } }
  • pub: An optional keyword to make the function public outside the module.

The return type can be omited.

Functions can be generic:

#![allow(unused)] fn main() { fn name<T>(arg: T) -> T { // ... } // call it let x: i32 = name::<i32>(2); }

Structs

Type functions

Creating a struct is simple enough:

#![allow(unused)] fn main() { struct Point { x: i32, y: i32 } }

Structs can be generic over types:

#![allow(unused)] fn main() { struct GenericStruct<T> { x: T, } }

You can associate functions to types using impl:

#![allow(unused)] fn main() { impl Point { pub fn new(x: i32, y: i32) -> Point { let point: Point = Point { x: x, y: y, }; return x; } pub fn add_x(&mut self, value: i32) { self.x = self.x + value; } } }

Enums

With the enum keyword you can define a enum:

#![allow(unused)] fn main() { mod option { enum Option<T> { Some { value: T, }, None, } impl<T> Option<T> { pub fn is_some(&self) -> bool { match self { Option#Some { value } => { return true; }, Option#None => { return false; } } } pub fn is_none(&self) -> bool { return !self.is_some(); } } } }
mod Enum { enum A { X { a: i32, }, Y { b: i32, } } fn main() -> i32 { let x: A = A#X { a: 2, }; let mut result: i32 = 0; match x { A#X { a } => { result = a; }, A#Y { b } => { result = b; } } return result; } }

Control flow

If

The if keyword allows conditional branching.

#![allow(unused)] fn main() { fn factorial(n: i64) -> i64 { if n == 0 { return 1; } else { return n * factorial(n - 1); } } }

For

A basic for loop:

#![allow(unused)] fn main() { fn sum_to(limit: i64) -> i64 { let mut result: i64 = 0; for (let mut n: i64 = 1; n <= limit; n = n + 1) { result = result + n; } return result; } }

While

The for keyword can be used as a while

#![allow(unused)] fn main() { fn sum_to(limit: i64) -> i64 { let mut result: i64 = 0; let mut n: i64 = 1; for (n <= limit) { result = result + n; n = n + 1; } return result; } }

Internal Details

Here you can find several internal and implementation details of how Concrete is made.

The Concrete IR

Currently in Concrete, the AST is lowered first to a IR to help support multiple targets and ease the generation of code on the end of the compilation process.

This IR is based on the concept of basic blocks, where within each block there is a linear flow (i.e) there is no branching.

Each block has a terminator, which for example can return or jump to another block, this is what defines the control flow of the program.

The IR struct

The IR struct holds the whole compile unit, with all the modules, structs, functions, types, etc defined within.

An arena is used to store the data types, so they can be cheaply referenced by the given typed index.

The IR stores all of these in a flat structure, i.e, functions defined in a submodule are available for lookup directly by their index through the arena.

The Module struct

Defines a module in concrete.

This structure holds the indexes of the functions, structs, modules, etc defined within this module.

The Function struct

Defines a function in concrete.

It holds the basic blocks and the locals used within.

The BasicBlock

It holds a array of statements and a terminator.

The statements have no branching.

The terminator defines where to branch, return from a function, a switch, etc.

The Statement

Currently there are 3 kinds of statements: assign, storage live, storage dead.

Only assign is used currently: it contains a place and a rvalue.

Place

This defines a place in memory, where you can load or store.

RValue

A value found in the right hand side of an assignment, for example the use of an operand or a binary operation with 2 operands, a reference to a place, etc.

Operand

A operand is a value, either from a place in memory or constant data.

Local

A local is a local variable within a function body, it is defined by a place and the type of local, such as temporary, argument or a return pointer.

The IR builder

This struct is used during the building of the IR, it holds structures needed only temporarely while building.

Some of these structures are:

  • A SymbolTable: which holds mappings from names to indexes.
  • The AST bodies of the major structures like functions, structs, etc. They are Arc thus cheaply clonable.
  • A mapping of top level module names to their module indexes.
  • The current self_ty self type to use during lowering when inside a method with a self argument.
  • The current local_module being built.
  • A struct index to type index mapping, to avoid duplicating and making equality easier.
  • A type index to module index mapping, to aid in method resolution.

The function IR builder

When lowering a function the IR builder is wrapper around a function builder, holding extra information to lower the function:

  • The function IR being build.
  • A mapping from name to local index (for named variables).
  • A list of statements
  • The return local index.
  • A hashset to know whether a local has been initialized (aids error checking).

Passes

Lowering is done roughly with the following passes:

Symbol resolution

A key concept is that the arenas holding the structures are Option<T>, this is needed to solve cyclic references.

The first pass loops through all the modules and all their structures, inserting them into the arenas with None (for everything expect modules) and storing the returned index to the symbol table.

Actually, within the pass, 2 loops are done, this is because for impl blocks we first need to have the struct and types ids assigned, so we can relate the methods to their types. So impl blocks are handled as a separate step.

Import resolution

The next pass handles imports between modules.

Module lowering

Here the whole lowering happens for all functions, structs, etc.