Cobalt Language Reference

Comments

Cobalt uses # for comments. Multiline comments start with # and at least one =. They end with at least the same number of =s.

comment_syntax.co
#=
This is a multi-line comment.
=#

# This is a single-line comment.

Multi-line comments

The syntax for a multi-line comment is:

multiline_comment_syntax.co
#=
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.

close_multiline_comment.co
#===
These won't close the comment:
=#
==#
But this will:
=======#

Multi-line comments can't be nested:

multiline_no_nesting.co
#==
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:

multiline_greedy.co
#===#
...
===#
In line 1, the parser takes #=== 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 (==#).

Built-in types
Numbers
SyntaxDescription
i{bits}signed integer with specified bits
isizesigned integer with same bits as a pointer
u{bits}unsigned integer with specified bits
usizeunsigned 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.

References and pointers

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:

call_with_ref_ptr.co
# 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.

SyntaxDescription
*Tpointer to type T: cannot change the memory being pointed to
*mut Tmutable pointer to type T: can change the memory being pointed to
&Treference to type T: cannot change the underlying instance
&mut Tmutable reference to type T: can change the underlying instance
Arrays
SyntaxDescription
T[]unsized array of objects of type T
T[{size}] sized array (with size size) of objects of type T
Unsized

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.
Practically, this means you will not use variables of type 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.
Sized

For sized arrays, size must be known at compile time.

Tuples
SyntaxDescription
(T, U, V, ...) tuple whose first element is type T, second is type U, etc.

You can access the members with parentheses:

tuple_access.co
let x: (i32, f32) = (3, 3.1415);
let x_int = x(0);
let x_float = x(1);
Structs
Literals

The simplest way to create a struct is to use literal notation.

struct_literal_syntax.co
let monster = { alive: true, health: 97i32, };

In the above example, monster has type {alive: bool, health: i32}.

You can access fields using dot notation:

struct_access_syntax.co
let health = monster.health;
Nominals

Structs can be named in the following way:

struct named syntax
type <NAME> = {
  <FIELD_NAME>: <FIELD_TYPE>,
  ...
};

To explicitly construct this, you can use a bitcast, or define a "constructor" which itself uses a bitcast.

struct_bitcast_syntax.co
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:

struct_transparent_syntax.co
@transparent
type MyStruct = {
  field1: bool,
  field2: i32,
};

fn main(): i32 = {
  let y = { field1: false, field2: 3i32 } :? MyStruct;

  y.field2
};
Symbol types

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:

symbol_type_syntax.co
# Syntax 1: a $ followed by any identifier:
$foo

# Syntax 2: a $ followed by a double-quote string literal:
$"a\nb"
Custom types

Types are defined using the typekeyword. The simplest form is this:

type_syntax.co
type <NAME> = <VAL>;

where

  • <NAME> is the name of the type you want to define.
  • <VAL> is the underlying representation.
This simple form is most useful for type-aliasing; i.e. providing a name for a preexisting type, potentially to hide the underlying properties of the preexisting type from users. For example,
opaque_ptr.co
type opaque_ptr = *null;
Construction

If you define a type like

type_syntax.co
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.

bitcast_examples.co
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.

Fields

Every type has a default field __base (note there are two underscores) which accesses the base type.

base_field.co
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

Methods are defined in the following way:

method syntax
type <NAME> = <VAL> :: {
  # Methods go here.
};

To define a method on an instance, use the self_t type:

method syntax with self
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:

method syntax with self
type <NAME> = <VAL> :: {
  @getter 
  fn val(self: self_t): base_t = self :? base_t;
};

Methods are often decorated with the following annotations:

AnnotationBehavior
@method General purpose, allows method to be called.
@getterAllows the method to be called like a field.
@op(drop)Implement a destructor (see below).
getter_example.co
@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
};
Destructors

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.

destructor syntax
type <NAME> = <VAL> :: {
  @op(drop)
  fn my_custom_destructor(s: &mut self) = {
      # Clean up here.
  };
};
Variables

Variables are defined with the let keyword. Types are deduced, but can also be manually specified:

var_def_syntax.co
let x = 3i32;       # deduced type
let y: i32 = 4i32;  # explicit type

Mutable variables are marked by the mut keyword:

mut_var_syntax.co
let mut x = 3i32;
x = 5i32;

Variables which are known at compile time can be specified using the const keyword:

const_var_syntax.co
const x = 5i32;
You do not include the let keyword.

Blocks and groupings

Blocks ({}) and groupings (()) can sometimes be used interchangeably, but differ in subtle ways.

Blocks

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.

block_syntax.co
# 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).

block_as_expr.co
# 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.

block_scope.co
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

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 (()).

groupings_ex.co
# 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):

invalid group
# Compiler error!
let x = (
  let y = 1;
);

In some cases, though, groupings and blocks can be used interchangeably.

group_block.co
# 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.

empty_group.co
# x has type/value null
let x = ();
Functions

Functions are defined as

function syntax
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 be null.
  • <EXPR> is an expression. Typically, this would be a block ({..}). If nothing is specified, this will default to null.
Parameters

Each parameter has the form

function parameter syntax
[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.
Reference parameters

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.

reference_param.co
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

Annotations can be placed above function definitions. The following options are available:

AnnotationBehavior
@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:

fn_annotation_example.co
# 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
};
Miscellaneous

  • If no body to a function is provided (and it is not extern) then the body defaults to null. In particular, the function can still be called.
Operators
SyntaxDescription
=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.
Bit cast

The bitcast operator (:?) results in a move:

bitcast_moves.co
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
};
Literals
Strings

String literals can be specified with double quotations:

bitcast_moves.co
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:

c_string_len.co
# length is 2
let s1 = "hi";

# length is 3
let s1 = "hi"c;
Integers

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:

integer_literals.co
# 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.

Floats

Float literals can be suffixed with a f followed by either 16, 32, 64, or 128.

Modules

Modules are a way of giving a scope and namespace to your code.

Same-file

The basic syntax is:

module same-file syntax
module <NAME> {
  <TOP_LEVEL_DECLARATIONS>+
};

Recall that a top level declarations can be functions and variable assignments. For example,

module_same_file_example.co
module MyModule {
  let my_constant = 3i32;

  fn my_function(): i32 = {
      6i32
  };
};
Using it

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_same_file_usage.co
module MyModule {
  let my_constant = 3i32;
};

fn foo(): i32 = {
  MyModule.my_constant
};
Imports

If/when that gets tedious, you can bring the module's contents into the current scope by importing it:

module_import_syntax.co
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_single.co
import MyModule.my_constant;

or multiple items like this:

import_single.co
import MyModule.{my_constant, my_other_constant};