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:
Currently there needs to be a top level module in every file.
Variables
You can declare a variable the following way:
Currently you always have to specify the type on the left hand side.
Functions
A function can be defined the following way:
pub
: An optional keyword to make the function public outside the module.
The return type can be omited.
Functions can be generic:
Structs
Type functions
Creating a struct is simple enough:
Structs can be generic over types:
You can associate functions to types using impl
:
Enums
With the enum
keyword you can define a enum:
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.
For
A basic for loop:
While
The for
keyword can be used as a while
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.