A Guide For C Programmers
Overview¶
This is intended for existing C programmers.
This primer is intended as a guide to how the C syntax – and in some cases C semantics – are different in C3. It is intended to help you take a piece of C code and understand how it can be converted manually to C3.
Functions¶
Functions are declared like C, but you need to put fn in front:
Find out more about functions, including named arguments and default arguments.
Calling C Functions¶
Declare a function (or variable) with extern and it will be possible to access it from C3:
Note that currently only the C standard library is automatically passed to the linker. In order to link with other libraries, you need to explicitly tell the compiler to link them.
If you want to use a different identifier inside of your C3 code compared to the function or variable's external name, use the @cname attribute:
extern fn int _puts(char* message) @cname("puts");
...
_puts("Hello world"); // <- calls the puts function in libc
New macro system¶
The old C macro system is replaced by a new C3 macro system.
Read more about semantic macros.
Identifiers¶
Naming standards¶
Name standards are strictly enforced, to simplify the C3 grammar:
// Starting with uppercase and followed somewhere by at least
// one lower case is a user defined type:
Foo x;
M____y y;
// Starting with lowercase is a variable or a function or a member name:
x.myval = 1;
int z = 123;
fn void fooBar(int x) { ... }
// Only upper case is a constant or an enum value:
const int FOOBAR = 123;
enum Test
{
STATE_A,
STATE_B
}
Variable Declaration¶
Multiple declarations are restricted¶
Multiple declaration with initialization isn't allowed in C3:
In conditionals, a special form of multiple declarations is allowed but each must then provide its type:
Zero initialization by default¶
In C global variables are implicitly zeroed out, but local variables aren’t. In C3 both global and local variables are zeroed out by default, but may be explicitly undefined (using the @noinit attribute) if you wish to match the C behaviour.
Removal of the const type qualifier¶
The const qualifier is only retained for actual constant variables. C3 uses a special type of post condition for functions to indicate that they do not alter input parameters.
<*
This function ensures that foo is not changed in the function.
@param [in] foo
@param [out] bar
*>
fn void test(Foo* foo, Bar* bar)
{
bar.y = foo.x;
// foo.x = foo.x + 1 - compile time error, can't write to 'in' param.
// int x = bar.y - compile time error, can't read from an 'out' param.
}
Expressions¶
Bit operator precedence changed¶
Notably bit operations have higher precedence than +/- and comparison operators, making code like this: a & b == c evaluate like (a & b) == c instead of C's a & (b == c). The elvis operator, ?:, also binds tighter than ternary. See the page about precedence rules.
0-prefix octal syntax removed¶
The old 0777 octal syntax present in C has been removed and replaced by a 0o prefix in C3, e.g. 0o777. Strings in C3 do not support octal sequences aside from '\0'.
Member access using . even for pointers¶
The -> operator is removed, access uses dot for both direct and pointer access. Note that this is just single access: to access a pointer of a pointer (e.g. int**) an explicit dereference would be needed.
In the special case of needing to dereference and index into an array, use .[] syntax:
int[3] a;
int[3]* b = &a; // Different from C!
// b[1] = 3; ERROR: expected an int[3] but got an int.
(*b)[1] = 3; // Works
b.[1] = 3; // Same as the above
This situation does not arise in C, due to pointer decay.
Signed overflow is well-defined¶
Signed integer overflow always wraps using 2s complement. It is never undefined behaviour.
Restrictions in implicit conversion rules¶
C3 does not permit implicit narrowing. Implicit widening is only allowed when there is only a single way to widen an expression.
Take the case of long x = int_val_1 + int_val_2. In C this would widen the result of the addition: long x = (long)(int_val_1 + int_val_2), but there is another possible way to widen: long x = (long)int_val_1 + (long)int_val_2. So, in this case, the widening is disallowed in C3. However, long x = int_val_1 is unambiguous, so C3 permits it just like C (read more on the conversion page).
Evaluation order is well-defined¶
Evaluation order (after precedence, meaning when operators have equal precedence, a.k.a. associativity) is left-to-right. In assignment expressions, assignment happens after expression evaluation.
int a = foo() + bar(); // Always evaluates foo() before bar()
*(baz()) = foo(); // foo() evaluates before baz()
Types¶
Struct, Enum And Union Declarations¶
Don't add a ; after enum, struct and union declarations, and note the slightly different syntax for declaring a named struct inside of a struct.
Also, user-defined types are used without a struct, union or enum keyword, as if the name was a C typedef.
Arrays¶
Array sizes are written next to the type, and arrays do not decay to pointers, you need to do it manually:
You will probably prefer slices to pointers when passing data around:
// C
int x[100] = ...;
int y[30] = ...;
int z[15] = ...;
sort_my_array(x, 100);
sort_my_array(y, 30);
// Sort part of the array!
sort_my_array(z + 1, 10);
// C3
int[100] x = {};
int[30] y = {};
sort_my_array(&x); // Implicit conversion from int[100]* -> int[]
sort_my_array(&y); // Implicit conversion from int[30]* -> int[]
sort_my_array(z[1..10]); // Inclusive ranges!
Note that declaring an array of inferred size will look different in C3:
Arrays are trivially copyable:
Find out more about arrays.C's typedef and #define become alias¶
C's typedef is replaced by alias:
alias also allows you to do things that C uses #define for:
// C
#define println puts
#define my_excellent_string my_string
char *my_string = "Party on";
...
println(my_excellent_string);
// C3
alias println = puts;
alias my_excellent_string = my_string;
char* my_string = "Party on";
...
println(my_excellent_string);
Find out more about alias.
typedef creates new types¶
typedef in C3 creates a new type with it's own methods, and the original type cannot implicitly convert to this new type, unless cast.
typedef MyId = int;
fn void get_by_id(MyId id)
{
return;
}
fn void test()
{
MyId valid = 7;
int invalid = 7;
get_by_id(valid); // allowed
get_by_id(invalid); // not allowed
}
Changes To enum and introducing constdef¶
C3 enums give new features, such as returning the name of the enum value at runtime. Their underlying representation always starts at 0 without gaps. For C enums with gaps, C3 uses constdef instead:
Read more about enums here.
Bitfields Are Replaced By Explicit Bitstructs¶
A bitstruct has an explicit container type, and each field has an exact bit range.
bitstruct Foo : short
{
int a : 0..2; // Exact bit ranges, bits 0-2
uint b : 3..6;
MyEnum c : 7..13;
}
There exists a simplified form for a bitstruct containing only booleans, it is the same except the ranges are left out:
For more information see the page on bitstructs.
Fixed size basic types¶
Several C types that would be variable sized are fixed size, and others changed names:
// C3
short a; // Guaranteed 16 bits
int b; // Guaranteed 32 bits
long c; // Guaranteed 64 bits
ulong d; // Guaranteed 64 bits
int128 e; // Guaranteed 128 bits
uint128 f; // Guaranteed 128 bits
usz g; // Same as C size_t, depends on target
sz h; // Same as C ptrdiff_t
iptr i; // Same as intptr_t depends on target
uptr j; // Same as uintptr_t depends on target
Find out more about types.
Type Qualifiers¶
Qualifiers like const and volatile are removed, but const before a constant will make it treated as a compile time constant. The constant does not need to be typed.
const A = false;
// Compile time
$if A:
// This will not be compiled
$else
// This will be compiled
$endif
volatile is replaced by macros for volatile load and store.
Modules¶
Modules And Import Instead Of #include¶
Declaring the module name is not mandatory, but if you leave it out the file name will be used as the module name. Imports are recursive.
module otherlib::foo;
fn void test() { ... }
struct FooStruct { ... }
module mylib::bar;
import otherlib;
fn void myCheck()
{
foo::test(); // foo prefix is mandatory.
mylib::foo::test(); // This also works;
FooStruct x; // But user defined types don't need the prefix.
otherlib::foo::FooStruct y; // But it is allowed.
}
No mandatory header files¶
There is a C3 interchange header format for declaring interfaces of libraries, but it is only used in special cases.
Comments¶
The /* */ comments are nesting
Note that doc contracts starting with <* and ending with *>, have special rules for parsing them, and are not considered a regular comment. Find out more about contracts.
C3 also treats #! on the first line as a line comment //.
Statements¶
goto Removed¶
goto is removed, but there is labelled break and continue as well as defer to handle the cases when it is commonly used in C.
// C
Foo *foo = malloc(sizeof(Foo));
if (tryFoo(foo)) goto FAIL;
if (modifyFoo(foo)) goto FAIL;
free(foo);
return true;
FAIL:
free(foo);
return false;
// C3, direct translation:
do FAIL:
{
Foo* foo = malloc(Foo.sizeof);
if (tryFoo(foo)) break FAIL;
if (modifyFoo(foo)) break FAIL;
free(foo);
return true;
};
free(foo);
return false;
// C3, using defer:
Foo* foo = malloc(Foo.sizeof);
defer free(foo);
if (tryFoo(foo)) return false;
if (modifyFoo(foo)) return false;
return true;
Changes To switch¶
casestatements automatically break.- Use
nextcaseto fallthrough to the next statement. - Empty
casestatements have implicit fallthrough.
Implicit break in switches¶
Empty case statements have implicit fall through in C3, otherwise the nextcase statement is needed. nextcase can also be used to jump to any other case statement in the switch.
For example:
We can jump to an arbitrary switch-case label in C3:
Undefined Behaviour¶
C3 has less undefined behaviour, in particular integers are defined as using 2s complement and signed overflow is wrapping. Find out more about undefined behaviour.
Other Changes¶
The following things are enhancements to C, that don't have an equivalent in C.
- Defer
- Methods
- Optionals
- Generic modules
- Contracts
- Compile time evaluation
- Reflection
- Operator overloading
- Macro methods
- Static initialize and finalize functions
- Dynamic interfaces
For the full list of all new features see the feature list.
Finally, the FAQ answers many questions you might have as you start out.