Cobalt Language Reference
Cobalt uses #
for comments. Multiline comments start with
#
and at least one =
. They end with at least the
same number of =
s.
#=
This is a multi-line comment.
=#
# This is a single-line comment.
The syntax for a multi-line comment is:
#=
This is a multi-line comment.
=#
You can close a multi-line comment with the same or more =
s than you started with, but not less.
#===
These won't close the comment:
=#
==#
But this will:
=======#
Multi-line comments can't be nested:
#==
There is no problem with writing
#========= yo! =#
even though it is an invalid multiline comment, since the
parser does not treat it as a comment (since it is contained
within a comment).
==#
The parser is greedy on =
s:
#===#
...
===#
#===
as the comment start, and #
as the next character. It does not, for example, see it as a comment start (#=
) followed by a comment end (==#
).
Syntax | Description |
---|---|
i{bits} | signed integer with specified bits |
isize | signed integer with same bits as a pointer |
u{bits} | unsigned integer with specified bits |
usize | unsigned integer with same bits as a pointer |
f{16|32|64|128} | floating-point number with specified bits |
bits
is any positive integer less that 65,536. Typically you want
to stick to a power of 2, and are probably good with using 32 or 64.
The primary difference between a reference and a pointer is that a pointer
needs to be dereferenced in order to act like the underlying value, while a
reference does not. For instance, suppose there is a type T
with
method foo()
. Here's how you would call this method with a
reference and with a pointer:
# Suppose x_ref has type &T.
x_ref.foo();
# Suppose x_ptr has type *T.
(*x_ptr).foo();
The underlying concept in play here is decaying. In this context, a
reference &T
"decays" into T
. You can think of this
as an implicit conversion.
Syntax | Description |
---|---|
*T | pointer to type T : cannot change the memory being
pointed to |
*mut T | mutable pointer to type T : can change the memory being
pointed to |
&T | reference to type T : cannot change the underlying
instance |
&mut T | mutable reference to type T : can change the underlying
instance |
Syntax | Description |
---|---|
T[] | unsized array of objects of type T |
T[{size}] |
sized array (with size size ) of objects of type
T |
For unsized arrays, there are two things to note:
T[]
must be behind a reference.- The size is stored with the data as a fat pointer.
T[]
directly, but rather use references &T[]
or
&mut T[]
. In this case, the reference can be thought of as a fat
pointer which stores the size of the array. In particular, you don't need to
keep track of the array size in a different variable.
For sized arrays, size
must be known at compile time.
Syntax | Description |
---|---|
(T, U, V, ...) |
tuple whose first element is type T , second is type
U , etc.
|
You can access the members with parentheses:
let x: (i32, f32) = (3, 3.1415);
let x_int = x(0);
let x_float = x(1);
The simplest way to create a struct is to use literal notation.
let monster = { alive: true, health: 97i32, };
In the above example, monster
has type {alive: bool, health: i32}
.
You can access fields using dot notation:
let health = monster.health;
Structs can be named in the following way:
type <NAME> = {
<FIELD_NAME>: <FIELD_TYPE>,
...
};
To explicitly construct this, you can use a bitcast, or define a "constructor" which itself uses a bitcast.
type MyStruct = {
field1: bool,
field2: i32,
};
fn main(): i32 = {
let y = { field1: false, field2: 3i32 } :? MyStruct;
# ...
};
However, keep in mind that naming a type like this obfuscates the base type.
What this means practically is that, to access the fields of a named struct,
you must either cast it to it's base type beforehand or mark it with the
@transparent
annotation:
@transparent
type MyStruct = {
field1: bool,
field2: i32,
};
fn main(): i32 = {
let y = { field1: false, field2: 3i32 } :? MyStruct;
y.field2
};
Symbol types are ZSTs (zero-sized types). They are defined "in place", meaning you don't need to have defined them somewhere else to use them. The syntax is as follows:
# Syntax 1: a $ followed by any identifier:
$foo
# Syntax 2: a $ followed by a double-quote string literal:
$"a\nb"
Types are defined using the type
keyword. The simplest form is
this:
type <NAME> = <VAL>;
where
<NAME>
is the name of the type you want to define.<VAL>
is the underlying representation.
type opaque_ptr = *null;
If you define a type like
type <NAME> = <VAL>;
and you are able to construct the type <VAL>
, then you can
cast the type <VAL>
into the type <NAME>
using a bit cast, e.g.
type MyInt = i32;
# Works
let x = 3i32;
let my_x = x :? MyInt;
# Also works
let my_y = 3i32 :? MyInt;
# Doesn't work-- you need to explicitly bit cast
let z = 3i32;
let my_z: MyInt = z;
You don't have to do this every time you construct your type. You can provide a method for construction. We'll discuss methods more below.
Every type has a default field __base
(note there are two
underscores) which accesses the base type.
type MyInt = i32;
fn main(): i32 {
let x1 = 3i32 :? MyInt;
let x2 = 3i32 :? MyInt;
# Two ways to access the base type.
let y1 = x1 :? i32;
let y2 = x2.__base;
0
};
Methods are defined in the following way:
type <NAME> = <VAL> :: {
# Methods go here.
};
To define a method on an instance, use the self_t
type:
type <NAME> = <VAL> :: {
@method
fn immut_method(s: &self_t);
@method
fn mut_method(s: &mut self_t);
@method
fn move_method(s: self_t);
};
You can refer to the type <VAL>
as base_t
:
type <NAME> = <VAL> :: {
@getter
fn val(self: self_t): base_t = self :? base_t;
};
Methods are often decorated with the following annotations:
Annotation | Behavior |
---|---|
@method | General purpose, allows method to be called. |
@getter | Allows the method to be called like a field. |
@op(drop) | Implement a destructor (see below). |
@C(extern)
fn puts(str: *u8);
type MyType = (i32, f32) :: {
@getter
@inline
fn x(s: &self_t): i32 = {
s.__base(0)
};
};
fn main(): i32 = {
let my_type = (3i32, 9.9f32) :? MyType;
if (my_type.x == 3i32) { # Note we don't use () to call a getter.
puts("Success");
} else {
puts("Failure");
};
0
};
A destructor is called when (after) an instance goes out of scope, and is responsible for cleaning up memory and closing resources associated with the instance. For basic types and their derivatives, you do not need to worry about implementing this yourself.
Destructors are implemented as methods annotated with @op(drop)
and accepting single &mut self_t
parameter. The return type is
null.
type <NAME> = <VAL> :: {
@op(drop)
fn my_custom_destructor(s: &mut self) = {
# Clean up here.
};
};
Variables are defined with the let
keyword. Types are
deduced, but can also be manually specified:
let x = 3i32; # deduced type
let y: i32 = 4i32; # explicit type
Mutable variables are marked by the mut
keyword:
let mut x = 3i32;
x = 5i32;
Variables which are known at compile time can be specified using the
const
keyword:
const x = 5i32;
let
keyword.
Blocks ({}
) and groupings (()
) can sometimes be
used interchangeably, but differ in subtle ways.
Blocks contain statements and evaluate to a value (of some
type). They are delimited by braces ({}
). Blocks are
themselves expressions. In particular, you may find that Cobalt requires
semicolons after blocks where other languages do not.
# This is a block:
{
let x = 1;
# This is a block within a block:
{
let y = 2;
};
let z = 3;
}; # semicolon!
Blocks are expressions with the same value as the last statement contained
within them. In the above example, the last statement in the outermost block
is let z = 3;
. This is a non-expression statement. Recall that
non-expression statements have value null
and type
null
. On the other hand, if the last statement in the block is an
expression, then the block is also an expression. In that case, the value of
the block will be the value of the last expression contained within it (and
the type of the value will be the type of the value of the last expression
contained within it).
# x has type/value null
let x = {
let y = 1;
};
# z has type i32 and value 5
let z = {
2i32 + 3i32 # uses i32 literal syntax
};
An important property of blocks is that they introduce a new scope. This is similar to the scoping behavior in other languages.
let x = 1;
let y = {
let z = 2;
# x is accessible in the inner scope
z + x
};
# z is not accessible in the outer scope
Groupings contain expressions, and are themselves expressions. Recall that every expression is a statement, but not every statement is an expression. In particular, groupings cannot contain non-expression statements.
Groupings are delimited by parenthesis (()
).
# x has type i32 and value 2
let x = (
2i32
);
# a has type/value null
let a = (
1 + 1;
);
# y has type i32 and value 2
let y = (
(
2i32
)
);
# b has type/value null
let b = (
# the value of an expression ending with a semicolon is null
(
2i32
);
);
This, however, is not valid (compare with blocks):
# Compiler error!
let x = (
let y = 1;
);
In some cases, though, groupings and blocks can be used interchangeably.
# x has type i32 and value 2
let x = {
2i32
};
# y has type i32 and value 2
let y = (
2i32
);
If a grouping contains no expressions, then it is an expression which evalutes
to null
.
# x has type/value null
let x = ();
Functions are defined as
fn <NAME>(<PARAM>*): <TYPE> = <EXPR>
where
<NAME>
is the name of the function.<PARAM>
is a function parameter. These are described in more detail below. By<PARAM>*
we mean that a function may have zero or more parameters. Each parameter must be separated by a comma.<TYPE>
is the return type of the function. If none is specified, don't include the colon : and the return type will be assumed to benull
.<EXPR>
is an expression. Typically, this would be a block ({..}
). If nothing is specified, this will default tonull
.
Each parameter has the form
[const|mut] <NAME>: <TYPE> [= <DEFAULT VAL>]
where
[const|mut]
means that, optionally, you can write const or mut before the type. const means the parameter is known at compile time. mut means the function can modify the parameter. If neither is specified, then you cannot change the value of the parameter.<NAME>
is the name of the parameter, which can be used inside the function.<TYPE>
is the type of the parameter.[= <DEFAULT VAL>]
means that, optionally, you can provide a default value to the parameter.
Note that having a reference parameter marked as mut
is not the
same as that parameter having type &mut Ty
. For instance, the
paramaters mut arg: &i32
and
arg: &mut i32
are not the same:
-
In the first case,
mut arg: &i32
, you can reassign what is being referenced by arg but cannot mutate the underlying object. -
In the second case,
arg: &mut i32
, you cannot reassign what is being referenced, but can mutate the underlying object.
To call a function with a reference parameter, you can either provide a reference or a value. If you provide a value then it will be automatically converted into a reference.
fn foo(x: &i32);
fn bar(y: &i32) = {
# Pass a reference parameter directly.
foo(y);
};
fn main(): i32 = {
let x = 3i32;
# Even though we pass in a value, since the function expects a reference it
# will convert it to a reference for us.
foo(x);
0
}
Annotations can be placed above function definitions. The following options are available:
Annotation | Behavior |
---|---|
@link(type) | set linkage |
@linkas(name) | set link name |
@cconv(convention) | set calling convention. can either be name or id |
@extern(cconv (optional)) | set function as externally defined |
@inline(always | never) | inline function. may or may not actually be done |
@C(extern (optional)) | unmangle link name and use C calling convention |
@target(glob) | only evaluate if the target matches the glob |
@export(true | false (optional)) | set module export, defaults to true |
@private(true | false (optional)) | does the opposite of @export |
@method (types only) | mark a function as a method |
@getter (types only) | mark a function as a getter |
For example, we can declare and use a function from libc
as
follows:
# Note both return type and body are inferred to be null.
@C(extern) fn puts(str: *u8);
# Note the allowed paramaters to main().
fn main(argc: i32, argv: **u8): i32 = {
puts("Hello, World!");
0
};
-
If no body to a function is provided (and it is not
extern
) then the body defaults tonull
. In particular, the function can still be called.
Syntax | Description |
---|---|
= | Ordinary assignment. |
+= | Add and assign. |
-= | Subtract and assign. |
*= | Multiply and assign. |
/= | Divide and assign. |
%= | Mod and assign. |
&= | Bit and, and assign. |
|= | Bit or, and assign. |
^= | Bit XOR and assign. |
<<= | Bit shift left and assign. |
>>= | Bit shift right and assign. |
The bitcast operator (:?
) results in a move:
type MyInt1 = i32;
type MyInt2 = MyInt1;
fn main() = {
let x = 3i32 :? MyInt1;
let y = x :? MyInt2; # x moved here.
# x is no longer valid.
0
};
String literals can be specified with double quotations:
let s = "Hello, world!";
A string literal can be immediately followed by c
. This used to
be required to pass string literals as parameters to (extern
)
functions expected C-style strings, but is no longer necessary. This is
because string literals are automatically appended by a null terminator.
However, by default, this does not affect the length of the string. If you
include the c suffix, it will:
# length is 2
let s1 = "hi";
# length is 3
let s1 = "hi"c;
Integers can be specified with suffixes beginning with i
or
u
, followed by the number of bits. If size
is
provided instead of a number (positive integer less than 65,536), then the
resulting integer will have the same size as a pointer. For example:
# signed integer with value 3 with size equal to 32 bits
let s1 = 3i32;
# unsigned integer of value 3 with size equal to that of a pointer
let s1 = 3usize;
If no suffix is provided, the type is inferred.
Float literals can be suffixed with a f
followed by either
16
, 32
, 64
, or 128
.
Modules are a way of giving a scope and namespace to your code.
The basic syntax is:
module <NAME> {
<TOP_LEVEL_DECLARATIONS>+
};
Recall that a top level declarations can be functions and variable assignments. For example,
module MyModule {
let my_constant = 3i32;
fn my_function(): i32 = {
6i32
};
};
To use the things defined in a module, you have a few options.
If the module is defined in the same file you want to use it, then you can prefix its elements with the module name, kind of like a namespace:
module MyModule {
let my_constant = 3i32;
};
fn foo(): i32 = {
MyModule.my_constant
};
If/when that gets tedious, you can bring the module's contents into the current scope by importing it:
module MyModule {
let my_constant = 3i32;
let my_other_constant = 5i32;
};
import MyModule.*;
fn foo(): i32 = {
MyModule.my_constant
};
The wildcard *
imports all elements of the module. You can import
a single item like this:
import MyModule.my_constant;
or multiple items like this:
import MyModule.{my_constant, my_other_constant};