Skip to content

The C3 Blog

C3 0.8.0 The Core Language Is Settling

C3 is a programming language that evolves C – the same low-level model but with modern ergonomics. With 0.8.0, we're getting our first real glimpse of what C3 1.0 will look like: the core language design is now locking into its final shape.

Two changes are big enough to deserve their own posts. The compile time reflection system has been reworked into its final form — and in the process we got to kill off half the builtins: link. And we're making the move to signed by default, correcting what turned out to be a five-year mistake: link.

As with all 0.x.0 releases, we've tried to collect all the breaking changes into this single release, so the rest of the 0.8.x line can stay backwards compatible.

Aside from the reflection changes, these are the major changes:

Distinct types

Distinct types now defaults to be "structlike"

0.7.4 introduced the concept of @structlike which meant a distinct type could not implicitly convert from a literal. With 0.8.0 we flip the defaults: by default distinct types do not convert from literals, but with @constinit they do:

// 0.7.4
distinct Foo7 = int;
distinct Bar7 @structlike = int;

Foo7 f = 1;      // Ok
// Bar7 b = 1; ERROR!
Bar7 b = (Bar7)1; // Ok 

// 0.8.0
distinct Foo8 @constinit = int;
distinct Bar8 = int;

Foo8 f = 1;      // Ok
// Bar8 b = 1; ERROR!
Bar8 b = (Bar8)1; // Ok 

Often people were assuming "structlike" behaviour, cause accidental bugs. For this reason we flipped the defaults.

Enums and constdef

No more +/-

Doing addition on enums is no longer possible:

// 0.7.4
MyEnum foo = BAR;
MyEnum baz = foo + 1; // OK!

// 0.8.0
MyEnum foo = BAR;
// MyEnum baz = foo + 1; ERROR!
MyEnum baz = (MyEnum)(foo.ordinal + 1); // OK

Enhanced ++/--

Enums still support ++/-- and get implicit wrapping when overshooting the ends:

enum MyEnum
{
    ABC,
    DEF,
    GHI
}
fn void test()
{
    MyEnum e = ABC;
    e++; // e is now DEF
    e++; // e is now GHI
    e++; // e is now ABC through overflow wrapping
    e--; // e is now GHI through underflow wrapping
}

Removed inline enums

To simplify the language, inline enums were dropped.

// Ok in 0.7.x, error in 0.8.0
enum YourEnum : inline int
{
    TEST
}

int x = YourEnum.TEST; // Valid due to inline

Implicit conversion to ordinal when used as index

Enums will now implicitly convert to ordinals when used as index:

enum YourEnum8
{
    HELLO,
    WORLD
}

fn void test()
{
    int[2] x;
    x[YourEnum8.WORLD] = 123; // Same as x[1] = 123
}

.nameof has become .description

enum Foo
{
    TESTING,
    THE,
    NEW,
    DESCRIPTION
}

// 0.7.11
String s = TESTING.nameof;      // "TESTING"

// 0.8.0
String s = TESTING.description; // "TESTING"

Constdef now infers through unary negations

Unary negation would previously prevent constdef inference

constdef Abc
{
    ABC = 4,
    DEF = -4
}

Abc a = ABC; // Ok
Abc b = -ABC; // Ok in 0.8.0
Abc c = ~ABC; // Ok in 0.8.0

Operator overloading

Overloads for comparisons

@operator(<) is now added, so that a type which overloads both < and == can participate in all comparisons.

@operator(!=) removed

@operator(!=) was removed as it had limited usefulness.

Compile time enhancements

Shadow an a = ... parameter if it's not defined

Shadowing is now allowed:

macro @foo(a = ...)
{
    $if !$defined(a)
        var a = 123; // Previously a shadowing error
    $endif
    return a * 2;    
}

Use $eval as the name of a named parameter

foo(arg: 2);

// Possible since 0.8.0:
foo($eval("arg"): 2);

untypedlist as a new builtin type

During compile time, some compile time arrays get the type "untypedlist", containing a (possibly heterogenous) list of values. Previously it was possible to create it, but it wasn't possible to directly reference the type. This changes with 0.8.0:

// 0.7.11 - 'var' is the only possibility
var $foo = { 1, "hello", 3.14 };

// 0.8.0
untypedlist $foo2 = { 1, "hello", 3.14 };

The main use is to be able to test whether an expression is an untyped list or not. Regular variables cannot have this type, it's a compile time type only.

Added a tags property

An often requested functionality is getting all the tags on a type or member. This is now finally possible in 0.8.0:

struct Foo @tag("a", "hello") @tag("b", "test")
{
    int x @tag("c", 5);
}

int a_global @tag("d", 3.14);

String[] $tags_of_foo = Foo::tags; // { "a", "b" }
String[] $tags_of_x = $reflect(Foo.x).tags; // { "c" }
String[] $tags_of_a_global = $reflect(a_global).tags; // { "d" }

Also note that name for retrieving and testing for tags have changed to get_tag and has_tag.

Allow taking the type of an interface method

It's now possible to take the type of an interface method:

interface TestInterface
{
    fn void hello_world(String name);
}

$Typeof(TestInterface.hello_world) x;

$expand compile time function

The new $expand compile time function allows turning any string to code. For this reason it's inherently LSP/IDE hostile, but simplifies cases where otherwise $exec was the only option:

import std::io;

// This code prints "Hello"
$expand(`fn void hello() { io::printn("Hello"); }`);

fn void main()
{
    $expand(@sprintf("%s();", "hello"));
}

Use with care.

Removal of $xxxxof style builtins, updated type access

This has its own article: link

Contracts

@mustinit

This attribute enforces initialization of a type:

struct Foo @mustinit
{
    int a;
}

fn void test()
{
    // Foo f @noinit; ERROR, must be initialized
    Foo f2; // Ok, zero initialization is fine
}

Generics

Generic inference now looks through pointers

This did not work in 0.7.11, but will infer {int} in 0.8.0:

// "create_buffered" is a generic function
BufferedChannel{int}* c = channel::create_buffered(mem, 1);

Nested generics in generic functions/methods

An omission prevented this from working:

fn List{List{Type}} return_nested_list() <Type>
{
    List{List{Type}} l;
    l.init();
    return l;
}

Syntax changes

Removal of deprecated syntax:

  • Removed iXX and uXX suffixes.
  • Removed Enum.lookup.
  • Removed ? as suffix operator in the expression io::EOF?.

?? and ?: has new precedence and binds tighter than + and -

This change is tied to the removal of ? as a suffix operator. Previously there were essentially parser hacks to get the right precedence in various situations, such as foo() ?? io::EOF?!. The new precedence places ?? and ?: tighter than + -, but looser than | & ^.

With this tighter precedence, some things change meaning:

// Parses as (foo() ?? io::EOF~) + 3 in 0.8.0
int a = foo() ?? io::EOF~ + 3;

// Parses as (b ?: 4) + foo() in 0.8.0
int a = b ?: 4 + foo();

// Still parses as as foo() ?? (b | 3)
int a = foo() ?? b | 3;

$Typeof and $Typefrom instead of $typeof, $typefrom

This is a simple name change, to make it clearer that they can be in a type slot, as opposed to being a value.

alias Foo = int::typeid now works

macro typeid @get_type()
{
    return SOME_CONST ? int : double;
}

// 0.7.11
alias Foo = $Typefrom(@get_type());

// 0.8.0
alias Foo = @get_type();

Toolchain changes

docgen command for html documentation generation

This major addition to the C3 compiler now allows you to produce high quality documentation out of the box. This is also (finally!) ensuring that this site has up to date docs: link.

Support for Emscripten

The C3 compiler now has experimental support for Emscripten out of the box.

Reduced library dependency scanning

Only used libraries are now scanned for dependencies. Let's say you have two libraries in your /lib folder "a.c3l" and "b.c3l", "b.c3l" depends on "c.c3l" which isn't in the folder. However, your code only uses "a.c3l". Previously this would have been an error, signalling it could not find "c.c3l" despite it not being used which complicated cross platform compilation setups.

Windows installer for the C3 compiler

To simplify getting the C3 compiler up on Windows for beginners, there's now a Windows installer.

Stdlib changes

Changes to API:

  • BufferedChannel and UnbufferedChannel are now pointers
  • JSON api changes: parse -> load, parse_string -> parse. It now supports two flavors of JSON: JSON and JSONC
  • std::math name changes: PI_2 => HALF_PI, PI_4 => QUARTER_PI, DIV_PI => INV_PI etc, cosec => csc, cotan => cot, muladd => mad
  • std::time name changes: diff_hour => diff_hours. DateTime.set_date => DateTime.set, datetime::from_date_* => datetime::at_*
  • std::hash method name convention changes: updatec / update_char => update_byte.
  • std::string name changes: strip => strip_prefix, strip_end => strip_suffix.
  • Ordering of object::new_* arguments are now "allocator first".
  • InStream.set_cursor is renamed seek, and the old seek is removed.
  • The Path API now is split into PathPosix and PathWin, Path is implicitly castable to String and loses the str_view() method. Use path::tnew instead of path::temp for a temporary path.
  • PanicFn now takes an int for row.
  • ElasticArray renamed FixedList.
  • Updated ref::new argument order.

Additions to the stdlib

  • Add SortedMap based on skip lists.
  • Add OneShotChannel to std::thread::channel for single-send/single-receive thread synchronization.
  • Add std::collections::Deque.
  • Ini parser and encoder.
  • Mergesort added.
  • std::encoding::xml for XML parsing and serialization.

  • Make DString.append_repeat polymorphic adding append_string_repeat and append_char_repeat. DString.append_inline for optimized uses.

  • List and FixedList adds a remove_unordered_at.
  • channel::create_unbuffered and channel::create_buffered to create channels
  • String.compare_to and String.compare_to_ignore_case added.
  • RingBuffer now conforms to foreach and adds additional functions.
  • conv::detect_bom, converts utf16/utf32 from bytes with byteswaped / unaligned data.
  • Object.to_value was added to convert an object to a value.
  • Add multi part and extension support to Path.append.

Fixes

The 0.8.0 release contains around 50 fixes to stdlib but also addressing issues around vector lowering.

Summarizing

This is just the diff between 0.7.11 and 0.8.0. Compared against 0.7.0 the list would be huge — which might raise a worry: is the language not stabilizing?

But the opposite is true. The difference between 0.7.11 and 0.8.0 is actually fairly small. Most of the work from 0.7.0 to 0.7.11 went into refinements — closing gaps in semantics and functionality, and responding to concerns from real-world use. If you're looking at C3 from the outside, a small breaking-change diff this late in the 0.7 series is a good sign: the design is settling, and 1.0 is coming into view. The docgen work is a key piece of that puzzle, and it's now in place.

For 0.8.x we're looking forward to continuing to flesh out the stdlib, improving generics inference, and tackling the remaining tasks needed for 1.0.

Thank yous

Again, this release wouldn't have been possible without the C3 community. I'd like to extend a deep thank you to all who have contributed, both through filed issues, PRs and just plain discussions.

I'd like to especially thank Manu Linares who this month single-handedly implemented the Docgen in the compiler, and refreshed the website as well.

PR contributors for this release

Stdlib: Christian Reifberger, cmann1, corleypc, Darvisim, Fernando López Guevara, Manu Linares, Mathis Laroche, Nyr24, Peng He, Sander van den Bosch, surrealism21.

Compiler & toolchain: Dmitry Atamanov, Fernando López Guevara, Johannes Müller, Manu Linares, Tomás Lopes, Zack Puhl,

CI/Infrastructure: Fernando López Guevara, Manu Linares, LowByteFox

Change Log

Click for full change log

Changes / improvements

  • Removed "old-enums, old-slice-copy and old-compact-eq" feature flags.
  • Removed deprecated $evaltype.
  • Removed all deprecated (as of 0.7.11) types and functions from the stdlib.
  • Removed deprecated iXX and uXX suffixes.
  • Removed deprecated Enum.lookup.
  • Removed deprecated ? as suffix operator in the expression io::EOF?.
  • Removed deprecated module foo {Type} generic syntax.
  • Distinct types now defaults to be "structlike"
  • Removed @structlike attribute.
  • Removed deprecated @extern attribute.
  • : in contracts before description is now mandatory.
  • Removed deprecated Enum.associated (use Enum::members).
  • Removed deprecated Enum.elements (use Enum::len).
  • Removed deprecated foo_function.params (use $reflect(foo_function).params).
  • Removed deprecated $is_const.
  • Removed deprecated $assignable.
  • Enums now no longer directly support + and - – use ordinals instead.
  • For enums, using ++ and -- will step through enums with implicit wrap-around.
  • Rename isz -> sz.
  • Make $sizeof, $alignof and all similar functions return sz instead of usz.
  • Align literal types with C semantics.
  • Use value promotion instead of signedness promotion to int. So that small unsigned types promote to int, not uint.
  • Add a @mustinit attribute to enforce zero-initialization of a type. #3094
  • Improve error message when keyword is used instead of an expression. #3088
  • Add --warn-recursivecontracts.
  • Mutex.destroy and friends no longer return optionals.
  • Remove @operator(!=) overload.
  • Add @operator(<) overload, enabling type comparison overloads.
  • Generic inference can now look through pointer.
  • Enums now implicitly convert to their ordinal when used as indices.
  • Enums can no longer declare themselves inline.
  • Nested generics allowed inside generic functions/methods.
  • a = ... parameters may be shadowed if not defined.
  • $eval can now be used with named parameters, e.g. foo($eval("arg"): 2) #3090
  • Type properties are now accessed using :: and the "of" suffix, removed: int.sizeof -> int::size
  • Added $reflect with properties name, cname, qname, offset, alignment, size.
  • Added @kindof, @alignof and @sizeof macros.
  • Removed $nameof, $extnameof, $qnameof, $offsetof, $alignof, $kindof, $sizeof.
  • .nameof is changed to .description on fault and enum types.
  • Type property is_eq is renamed has_equals.
  • Type function tagof is renamed get_tag.
  • Add untypedlist as a usable type #2647.
  • ?? and ?: has new precedence and binds tighter than + and -
  • Added the tags property for types and $reflect.
  • Allow taking the type of an interface method.
  • Add $expand compile time function to convert strings to code.
  • Constdef now infers through unary negations.
  • Only used libraries are scanned for dependencies. #3144
  • $vaconst, $vaexpr and $vatype removed.
  • Improve error message on unsupported typeid runtime access at runtime. #3170
  • Added support for Emscripten.
  • Replace $vacount by $vaarg.len, replace $vasplat by ...$vaarg.
  • $vaarg behaves as $vaexpr.
  • Added docgen command to generate documentation.
  • Added jmpabs x86 CPU feature.
  • Implicit unsigned <-> signed integer conversions removed.
  • Added C3 Compiler setup installer for Windows
  • alias Foo = int::typeid now works.
  • $typeof => $Typeof, $typefrom => $Typefrom.

Stdlib changes

  • Add List.remove_unordered_at.
  • PanicFn now takes an int for row.
  • Add std::collections::Deque.
  • Add compare_to and compare_to_ignore_case to String. #3096
  • Add SortedMap based on skip lists.
  • Add OneShotChannel to std::thread::channel for single-send/single-receive thread synchronization.
  • BufferedChannel and UnbufferedChannel are now pointers, create using create_unbuffered and create_buffered
  • RingBuffer now conforms to foreach and adds additional functions.
  • Ini parser and encoder.
  • Updated ref::new argument order.
  • Support setting thread stack size.
  • Support setting thread priority.
  • Support syscall on RISCV.
  • Make DString.append_repeat polymorphic adding append_string_repeat and append_char_repeat.
  • Add DString.append_inline for optimized uses.
  • Ordering of object::new_* arguments are now "allocator first".
  • Add remove_unordered_at to FixedList.
  • Changed json to support two flavors of JSON: JSON and JSONC.
  • Changed json API: parse -> load, parse_string -> parse.
  • conv::detect_bom, convert utf16/utf32 from bytes with byteswap / unaligned data.
  • Mergesort added.
  • set_cursor is renamed seek, and the old seek is removed.
  • std::math name changes: PI_2 => HALF_PI, QUARTER_PI => PI_4, DIV_PI => INV_PI etc, cosec => csc, cotan => cot, muladd => mad
  • std::time name changes: diff_hour => diff_hours. DateTime.set_date => DateTime.set, datetime::from_date_* => datetime::at_*
  • std::hash method name convention changes: updatec / update_char => update_byte.
  • std::string name changes: strip => strip_prefix, strip_end => strip_suffix.
  • std::collections::object added Object.to_value to convert from an object to a value.
  • std::encoding::xml added for XML parsing and serialization.
  • Fix Path.append separator not honoring the specified environment.
  • Add multi part and extension support to Path.append.
  • The Path API now is split into PathPosix and PathWin, Path is implicitly castable to String and loses the str_view() method. Use path::tnew instead of path::temp for a temporary path.

Fixes

  • Slice comparison lowering would not work correctly in macros in some cases. #3095
  • Attributes @allow_deprecated, @constinit, @noalias, @nostrip, and @optional would erroneously accept parameters. #3098
  • Fix pipe handle leaks across concurrent process spawns #10067.
  • $$trap was incorrectly marked noreturn.
  • Recursive inclusion of contracts was not detected.
  • \r was not filtered when piping a source file from stdin.
  • SHA-3 and Keccak contexts are now explicitly @mustinit structures. #3110
  • UnbufferedChannel would deadlock on multiple producers.
  • Don't override sigaltstack when running with --sanitize=address. #3115
  • Binary search broken for some supported functions.
  • Fix bug casting (void*[<3>])x.
  • Compiler crash compiling a switch with a constant case range overlapping a constant case value. #3127
  • Incorrect handling of overaligned struct fields #3136
  • EnumSet with more than 128 entries was broken.
  • Handle underflow in zip.
  • Bugs in check for name suggestions on name mismatch.
  • Fix bug where only one ensure would not be inlined correctly. #3162
  • Incorrect error message when casting to non-existent enum.
  • Macro $Type = ... would not work correctly with $defined
  • Fix enum value handling in Object (std::collections::object) to conform with changes in enums.
  • Compiler assert in certain cases with ?? and void returns. #3168
  • Bug in compiler-rt for i128 shift.
  • LinkedBlockingQueue.push_timeout did not work correctly.
  • Splat into vaarg macro, where vaarg is not used #2782.
  • Comparison with floats had incorrect codegen, leading to incorrect results for NaN #3175.
  • Zeroing out simd vectors in a struct could in some cases lead to incorrect lowering #3179.
  • Incorrect lowering when returning a struct to an optional value on Win64 in some cases #3180.
  • Fix bug where a method is considered doubly generic if declared in a generic module for a generic type. #3176
  • Fix exp10 on platforms without exp10 as an LLVM builtin.
  • LLVM 23 compatibility: map Os/Oz to O2 pass pipeline, fix returnaddress intrinsic signature, add optsize/minsize function attributes.
  • Warning for ignored visibility modifiers was not emitted for macro methods #3071
  • while (String? x = foo()!) was accidentally allowed causing a lowering error.
  • Crash casting uint to bitstruct inside struct field assignment #3187
  • Vec2/Vec3 transform missed matrix translation.
  • Matrix rotation ignored matrix itself.
  • Fix BigInt shr, to_format, and others.
  • Fix ends in TDist.quantile, FDist.pdf, ChiSquaredDist.pdf
  • Fix to easing expo_in and bounce_inout.
  • deque with shrinking a zero sized list caused infinite loop.
  • Printing an enummap yielded the wrong character count.
  • Incorrect contract in FixedList allowed insert out of range.
  • Fix double-free in InterfaceList.
  • Object.set_at was incorrect.
  • Bitstruct with backing char[n] would occasionally be incorrectly stored.
  • fmuladd lowering crashes on a + -(b * c) with fastmath.
  • Constant folding -30 % -7 would incorrectly yield "2".
  • Parsing << in asm would not be correctly handled.
  • Incorrect lowering for float[<3>] when placed aligned in a struct.

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.

Let's kill off half the builtins in 0.8.0

Goodbye $sizeof, $alignof and all the rest

The problem

From the beginning, C3 mirrored C in having a sizeof builtin, which would take an expression and return the size. This was extended with additional similar builtins that would take an expression and return some reflection information about it.

To avoid colliding with anything else, it would get the -of suffix, and in line with C3's "$ means compile-time", they were prefixed with $.

At some point this became an actual challenge for naming things. $paramstructof doesn't quite roll off the tongue.

The solution: $reflect

So starting from 0.8.0, all of those are removed in favour of using the new $reflect builtin containing all attributes. For example, our $sizeof(a) becomes $reflect(a).size. In general, *of(a) becomes $reflect(a).*.

This might seem onerous, but first of all this allows us to store the reflection data and pass it to macros: some_macro($reflect(a)). But it's also trivial to construct macros that do the same thing. For example, @sizeof and @alignof are new macros that act as drop-in replacements for the previous builtins.

In 0.7.x you would use things like $defined($sizeof(x)) to test if something had a size defined. After the changes in 0.8.0, this becomes $defined($reflect(x).size) instead.

This change allows us to reduce the number of keywords and overall "language surface". Without explicit keywords, compile-time reflection actually becomes more flexible as well – it's easier to introduce additional introspection at some later time in the 0.8.x cycle if needed.

Stripping types of their properties

The problem

Up to 0.7.x, the corresponding type properties (related to the expression reflection previously discussed) were accessed using dot notation, e.g. int.sizeof. This reflected the corresponding builtin: int.sizeof <=> $sizeof(x), int.alignof <=> $alignof(x) and so on.

It would certainly have been nicer to use int.size, but properties would shadow method names:

struct Foo
{
    int len;
}

fn int Foo.size(self)
{
    return self.len;
}

fn void test()
{
    Foo f;
    Foo.size(f); // Valid C3    
}
So the .*of suffix was used exactly to avoid colliding with methods and field names.

However, as previously mentioned, naming with "-of" was increasingly a blocker. Could something be done?

The solution: :: to the rescue

The :: scope operator unequivocally separates module names from an identifier, and module names are always lower case. mymodule::submodule::Bar always means that everything left of the last :: is a module.

But what if we allowed Bar::baz?

This had no meaning in C3, because "Bar" could never be a module name.

In fact, int::size could not match anything. So what if we made :: the way to get type properties?

int::size now works, because the only thing that can appear to the right of a type name after :: is a type property.

// 0.7.11
const INT_SIZE = int.sizeof;
const INT_ALIGNMENT = int.alignof;
const INT_MAX = int.max;
// 0.8.0
const INT_SIZE = int::size;
const INT_ALIGNMENT = int::alignment;
const INT_MAX = int::max;

So the change both allows dropping "-of", and make it more consistent – like in the int.max case.

The big $vaarg reduction

The problem

The final big removal in C3 is the elimination of $vaexpr, $vacount, $vaconst, $vatype and $vasplat.

These builtins helped support macro "raw" vaargs. $vaexpr would paste in a vaarg as if it was declared like #foo, $vaconst like $foo and $vatype like $Foo.

Unfortunately, most of these were sparsely used.

$vasplat did splat the arguments, perfectly forwarding them into an initializer or another call.

However, $vasplat was conceived well before most of the ... splat functionality was in the compiler. In fact, it could be considered the precursor of most of the splat code.

The fact that it looked so very different from regular splats seemed unnecessary.

And $vacount? A keyword just to get the number of vaargs available?

The solution: one $vaarg to rule them all

While $vaarg did an adequate job to try to ensure that the expression was only evaluated once, the raw vaargs for macros largely behaved like lazy expression parameters ("#foo"). It was also always possible to recreate types and constants from $vaexpr as needed.

So if we just had $vaexpr, almost all functionality would still be available!

Since we already have ... for splat and .len to get length, we could derive everything from a single value. We renamed $vaexpr and called it $vaarg:

// 0.7.11
$for var $i = 0; $i < $vacount; $i += 2:
    self.set($vaarg[$i], $vaarg[$i + 1]);
$endfor

call_log(CRITICAL, category, fmt, $vasplat);

// 0.8.0
$for var $i = 0; $i < $vaarg.len; $i += 2:
    self.set($vaarg[$i], $vaarg[$i + 1]);
$endfor

call_log(CRITICAL, category, fmt, ...$vaarg);

Final thoughts

There is a natural contract-expand cycle to language design: first add features, then learn how they're used and how they can be simplified for the actual real use cases.

The builtins we're removing solved real problems, but they also added surface area: extra keywords, naming conventions, subtle interactions.

These simplifications were only possible now, after things like maturing the splat functionality, observing $vaarg usage-patterns and reaching the point where the old type property syntax prevented growth.

We hope you'll like them!

Unsigned sizes: a five year mistake

A quick note for readers who don't follow C3: it's a systems language in the C tradition. Specifics below are C3's, but the tradeoffs apply to any language that has to pick a type for sizes and lengths.

C3 is moving to signed by default, but why are we doing that? Isn't unsigned more correct for sizes at least? Let's try to answer that.

The bugs of unsigned

Since the early days, C3 has been using unsigned sizes. And while the name of the unsigned type changed over time – from "usize" to "usz" (after the unification with the uptrdiff type) – its position as the default has been unchallenged.

However, unsigned has known pitfalls, the most well known being:

for (uint x = 10; x >= 0; x--) // Infinite loop!
{ ... }

In fact, that bug is so easy to run into that C3 explicitly rejects x >= 0 for unsigned types outside of macros.

Another classic C bug is:

uint a = 0;
int b = -1;

if (a > b) { ... }

In C, both will promote to unsigned, turning b into a huge unsigned value, causing the comparison to fail. For this reason, C3 implemented safe unsigned/signed comparisons that wouldn't convert both sides and be safe regardless.

C, of course, allows implicit conversions between unsigned and signed. While this is a source of bugs, I felt that with some safety measures, it could mostly be kept.

It's easy to think that the bugs above are just unrelated quirks. The loop that never terminates, the broken comparison, the conversions that need to be fixed just-so... they all stem from one earlier decision: that unsigned should be the default for sizes. Most of this post is really about that decision.

A pertinent question

You might reasonably ask "but why not just require that signed/unsigned conversion is explicit?".

The reason, it turns out, lies with unsigned sizes.

If sizes are unsigned, like in C, C++, Rust and Zig – then it follows that anything involving indexing into data will need to either be all unsigned or require casts. With C's loose semantics, the problem is largely swept under the rug, but for Rust it meant that you'd regularly need to cast back and forth when dealing with sizes.

There are two approaches to casts: one is to liberally sprinkle them all over the codebase with the idea that "it's an explicit conversion, so it's obvious what happens". The other is to minimize casts, only using them to signal that something out of the ordinary is happening: "here be dragons".

The former is easier to define, but has the downside of essentially "silencing warnings". Let's say the code was originally written to cast an u16 to u32, but later the variable type changes from u16 to u64 and the cast is now actually silently truncating things. Here we have casts becoming a sort of "silence all warnings".

The main idea of "it's an explicit conversion" is also undermined by the practice of just putting in casts mechanically where the compiler says they're needed, rather than trying to actually examine every case.

On the other hand, minimizing casts is more challenging: rules are needed to correctly allow "safe" implicit casts, while requiring casts for what's unsafe.

C3 takes the second approach: casts should mean something, but why did it allow unsigned <-> signed? Isn't that unsafe?

It turns out that as long as you only use addition, subtraction and multiplication it's mostly quite safe if signed integers are 2s complement. And given that the conversions would need to happen often (remember: unsigned sizes!), the trade-off to make it implicit was natural.

The best laid plans

C3 has largely kept the current conversion semantics since 2021, and had been working reasonably well without triggering any serious undesirable semantics for 5 years, until an innocent question about "(foo + a) % 2" turned those assumptions on its head.

In order to remove footguns, C3 had picked "int + uint" to promote to "int" instead of unsigned. This made a lot more cases silently signed, which tended to be the correct thing in most cases. But what if we do (foo + a) % 2 and "foo" here happens to be over INT_MAX? Suddenly we get incomprehensible results! The right answer being (foo + a) % 2U instead.

This was an unacceptable problem. Not because it was hard to fix, but because it was so surprising. Almost everywhere else you could simply ignore if there was an underlying conversion to signed or not – it just worked. But / and %? Here's where the solution broke down. And because it "just worked" everywhere, it was fairly opaque what subexpression was signed or unsigned. The convenience made this minor issue into a big one.

The immediate reaction to this was to patch it: just issue an error on doing "unsigned / signed" and "unsigned % signed", but more issues were lurking in the shadows.

The tricky wrap

If you write a ring buffer, how do you make sure that calculating offsets are wrapping?

The naive solution is this:

index = (start + offset) % length;

This works as long as offset is positive. What about negative values? Here's a common simple solution:

index = ((start + offset) % length + length) % length;

Since offset is negative, we can assume signed numbers, so barring extremely large offsets (causing signed overflow) this will work.

(Note: if % had yielded the modulo rather than the remainder, the naive solution would have worked)

Now, remember how we started with unsigned sizes? Unsigned first will likely lead us to using all unsigned, leading to code looking like this:

index = ((start - offset_back) % length + length) % length;

Which is completely wrong – but also hard to detect. It will sometimes wrap correctly, but mostly not.

The correct code for unsigned needs to look something like this:

index = (start + length - (offset_back % length)) % length;

Regardless of what rules we apply to unsigned and unsigned-signed conversions, there is simply no way for the compiler to let us know that the first "offset_back" example is broken for unsigned.

So let's back up a bit.

The unsigned size

It seems hard to solve the problem with unsigned, so is there some faulty assumption we're making?

Let's look back in time: C initially was about using signed integers being designed around the int type. This all changed when the type of sizeof was standardized to the unsigned size_t.

This single change single-handedly introduced unsigned arithmetics as a common thing in C code. Finding this new shiny thing, people started to use unsigned to encode "this value can't be negative" and talk about how using unsigned helped since it allowed them to express larger sums.

That didn't mean it was without problems. In fact the problems were so significant that in the 90s, Java decided to drop unsigned types entirely in its design. Java's reaction was perhaps a little extreme, but it did achieve the goal of making a large set of common bugs – related to unsigned – just go away.

Go should give us pause: it's a low-level language, created as a reaction to problems in C++, by people who knew exactly what unsigned sizes cost - and they picked signed sizes.

With any bounded integers, problems arise when we close in on the boundaries. For a 32-bit signed int that is approximately at plus and minus 2 billion, for an unsigned 32-bit integer, it's at 0 and approximately 4 billion. The "unsafe" boundaries for unsigned lie so much closer than for signed integers – there is simply no contest.

This is exactly why we see problems for things like in the case with %.

But what about the range? While it's true that you get twice the range, surprisingly often the code in the range above signed-int max is quite bug-ridden. Any code doing something like (2U * index) / 2U in this range will have quite the surprise coming. But it's worse than that: overflow for signed values generally produces an invalid, negative number – but unsigned overflow often produces a quite plausible number, just the wrong one. Not to mention that on modern 64 bit machines, you'll run out of memory before you can use a full signed 64 bit integer.

Ok, but isn't it valuable to be in the right range by design? The answer seems to be no judging from work on verification frameworks, as unsigned only encodes modulo behaviour and actual ranges. It might be argued that you can make unsigned overflow an error (this is indeed what Rust does), but that removes useful properties of unsigned arithmetics: (a + b) - c is equal to a + (b - c) if unsigned arithmetics wrap, but is not the same if it doesn't. This is a trap in itself.

So we have unsigned quite frequently used, more or less by historical accident. It's error prone and silently hides errors. So maybe the solution isn't trying to make it more ergonomic?

Signed first

As you might have anticipated, C3 has come to adopt signed sizes for types and lengths. Since unsigned now becomes more rare, we don't need any implicit conversions between unsigned and signed. Comparisons between unsigned and signed? – also gone.

When doing this change I started removing unrelated uint and ulong usages as well, and I discovered code that seemed suspicious or just plain wrong. Also, code just got plain simpler with just int and signed sizes everywhere. This is where I realized I had been internalizing the cost of using unsigned: after a while working in C or C++, you get the habit of looking for possible problems due to unsigned, and using patterns that are less obvious, but are sure to work for both unsigned and signed variables.

I'm a bit embarrassed about how long it took for me to change this, and it's a testament to how deeply ingrained the habit was. I just assumed unsigned sizes was the way to go, and that the problem was simply to improve ergonomics and eliminate as many pitfalls as possible. This despite both Go and Java showing the way with signed sizes.

But even after deciding on the change, converting from unsigned to signed felt awkward and wrong at first, as if I was doing something forbidden – that's how far gone I was. But seeing how each change both made the code easier to reason about and more correct, I couldn't deny the evidence.

Some notes on the changes in C3

This change was discussed in the C3 discord before it was implemented and got the affectionate name "iszmageddon", this is in reference to the isz type (corresponding roughly to ssize_t) becoming the default type of sizes.

In order to more clearly promote the signed size, it was renamed just "sz", giving 0.8.0 the asymmetric pair sz/usz. This makes it easy to remember which one is preferred. Consequently the change was renamed "szmageddon".

Originally the implicit conversion between signed <-> unsigned was mainly left intact, but it was later completely dropped.


Discuss this article on Hacker News.

C3 0.7.11 - The last v0.7

With 0.7.11 we've reached the end of the 0.7 era. It's been a really good year for C3, improving on rough edges and expanding the stdlib.

This release is headlined by an updated matrix library. Aside from that, it’s a refinement release, bringing numerous fixes but no major changes.

Language changes

constdef inference through binary operations

constdef is often used to define masks:

constdef Foo
{
    AUDIO = 0b01,
    VIDEO = 0b10,
    /* ... */
}

In 0.7.10 inference worked through assignment but not expressions:

Foo f = AUDIO; // Ok
f = Foo.AUDIO | Foo.VIDEO; // Ok
// f = AUDIO | VIDEO - this is an error

From 0.7.11 onwards, the inference works:

f = Foo.AUDIO | Foo.VIDEO; // Ok
f = AUDIO | VIDEO; // Also ok in 0.7.11

Updated @weak

@weak, which changes linkage, now also supports having multiple definitions of the same declaration in the same source code, allowing the non-"weak" definition to win. This allows things like changing code from this:

fn void foo() @if(env::POSIX)
{
    io::printn("Works!");
}

fn void foo() @if(!env::POSIX)
{
    abort("Unsupported foo()");
}
To this:
fn void foo() @if(env::POSIX)
{
    io::printn("Works!");
}

fn void foo() @weak
{
    abort("Unsupported foo()");
}

Warning on $$builtin use

Builtins (functions prefixed with $$, such as $$unreachable) are intended to be accessed through standard library macros, not used directly. They are considered internal and may change without warning.

This wasn’t previously clear, and some code ended up using them directly. The compiler will now issue a warning when such builtins are used outside the standard library.

Zero element enums forbidden

Before 0.7.11, the language allowed empty enums. In practice, they were not fully supported and could lead to incorrect behavior.

Given their limited usefulness and the inability to define a valid zero value, empty enums have been removed from the language.

Misc improvements

C3 now also detects large temporaries when creating slices on the stack.

Standard library

Updated Matrix library

The big change is the updated Matrix library. The new matrix type is column major, aligning it with most graphics and math libraries. It has also undergone quite an overhaul, with methods and functions updated and fixed. The default aliases are now based on floats rather than doubles, which fits with common usage.

The predefined aliases are:

  • Quat - quaternion
  • Mat2 - 2x2 matrix
  • Mat3 - 3x3 matrix
  • Mat4 - 4x4 matrix
  • Vec2 - 2d vector
  • Vec3 - 3d vector
  • Vec4 - 4d vector
  • Rect - 2d rectangle

The matrix perspective and ortho, project and unproject functions are now right-handed [0,1].

std::mem improvements

  • std::mem::allocator is deprecated and split into std::core::mem::allocators containing allocators and std::core::mem::alloc for various allocation methods. This means replacing most allocator::* calls with alloc::*, e.g. allocator::calloc becomes alloc::calloc.
  • @unaligned_load and @unaligned_store are deprecated in favour of mem::load and mem::store, which also supports unaligned volatile load/store operations.

std::encoding improvements

  • Serialization to and from structs using JSON is now available.
  • Functions for gzip compression and decompression added.
  • Support for AES encrypted Zip files
  • Base32/Base64/Hex/Codepage encoding deprecated encode_buffer/decode_buffer, replacing them with encode_into and decode_into.

Crypto / hashes

  • Keccak, SHA3, Shake, CShake, kmac, Turboshake, Tuplehash and Parallelhash were added.
  • The remaining Xoshiro and xoroshiro PRNG variants were added.
  • Added Argon2 hash.
  • entropy module for generating cryptographically secure random bytes.
  • random::seeder no longer uses temp memory.

Date / Time

  • DateTime and DateTimeTz are now Printable and can be used directly with printf
  • DateTime now has a to_format method.

Misc improvements

  • The backtrace has been cleaned up on Linux.
  • ZString now has a hash method.
  • Simple member-wise struct comparison using member_eq.
  • always_assert macro, which asserts even in unsafe mode.
  • file::last_modified was added.
  • SubProcess was renamed Process and refreshed with new, more streamlined, functions.
  • Use methods short_name() and @short_name() to get the unqualified fault name, e.g. io::EOF becomes EOF.

Toolchain

Removal of support for LLVM 17/18 and fixes for LLVM 22

On most platforms, the C3 compiler is linked with custom-built LLVM libraries, which reduces the need to support older versions of LLVM.

0.7.10 was incompatible with LLVM 22, which is fixed in this version.

Fully static builds of C3C for Linux

With 0.7.11, MUSL-based builds of the compiler are available on Linux.

Unified SDK fetching with Android support

0.7.10 brought automatic download of the MSVC SDK without needing external scripts. In 0.7.11, this is extended to support Android, with the goal of bringing in more targets, such as the MacOS SDK for effortless cross-compilation.

Bug fixes

0.7.11 brings many major and minor bug fixes, see the complete release notes for details.

Looking Forward

Next up will be 0.8.0. As usual, this means there will be breaking changes. The most obvious target is all the deprecated functionality, from enum Foo : const int to the old matrix library.

However, the big upcoming change is the so-called "Szmageddon": C3 will change from unsigned sizes (usz) to signed sizes by default. This will also affect type promotion rules and literal types.

Community and Contributions

This release wouldn't have been possible without the C3 community. I'd like to extend a deep thank you to all who have contributed, both through filed issues, PRs and just plain discussions.

PR contributors for this release:

Stdlib: Alexandru Paniș, Book-reader, Bram Windey, Dave Akers, Disheng Su, Flanderzz, Konimarti, Manu Linares, LowByteFox, Siladi, Skunky, Technical Fowl, Zack Puhl

Compiler & toolchain: Dmitry Atamanov, Lucas Alves, Manu Linares, Nmurrell07, Rodrigo Camacho

CI/Infrastructure: Manu Linares, Mehdi Chinoune, Sisyphus1813, Zack Puhl

Change Log

Click for full change log

Changes / improvements

  • Removed support for LLVM 17, 18.
  • Detect large temporaries when creating slices on the stack #2665
  • Search for the linker in PATH; use the builtin linker if CC missing. #2906
  • constdef inference through binary expressions: Foo f = Foo.AUDIO | Foo.VIDEO can be written Foo f = AUDIO | VIDEO;
  • Fix for LLVM 22+ compatibility #2987
  • @weaklink for just affecting linkage.
  • Add a fully static build of c3c for Linux. #2949
  • @weak now allows direct overriding of @weak definitions with a real definition.
  • Unified SDK fetching under c3c fetch-sdk <target> (windows, android) and added support for automatic Android NDK (r29) download. Better progress bar. #3019
  • Improved Linux backtrace readability by stripping internal panic and runtime startup frames. #3008
  • Added repetition compression for deep recursive stacks in backtraces. #3008
  • Added new builtins: $$acos, $$asin, $$atan, $$cosh, $$exp10, $$sinh, $$tan and $$tanh.
  • Added the rest of the xoshiro and xoroshiro PRNG variants. #3027
  • Improve error when using keyword as identifier #3066
  • Warn when using $$builtin functions outside of the stdlib #3065
  • Zero element enums now disallowed.

Stdlib changes

  • Add contract on any_to_enum_ordinal and any_to_int to improve error when passed an empty any. #2977
  • Add hash method for ZStrings. #2982
  • Added json serialization from structs.
  • Add keccak and Keccak-based hash functions: sha3, shake, cshake, kmac, turboshake, tuplehash, and parallelhash. #2728
  • Added fault.short_name and fault.@short_name to get just the fault name for both run and compile time. #3002
  • Compiler runtime functions extracted outside of std.
  • Add the GZIP file format (RFC 1952).
  • Add file::last_modified.
  • Make DateTime and DateTimeTz Printable.
  • Add to_format functionality for DateTime.
  • SubProcess/process::create/process::execute_stdout_to_buffer deprecated, replaced by Process/process:spawn/process::run_capture_stdout.
  • Add support for AES-encrypted Zip files (AE-1 and AE-2 formats).
  • Add Argon2 memory-hard hashing with associated tests. #2773
  • Matrix type is now column major.
  • Fix matrix perspective and ortho, project and unproject to be RH [0,1]
  • Add vec3 methods: rejection, project, implement unproject.
  • Add vector function cubic_hermite
  • Deprecated sq_magnitude, barycenter, towards, ortho_normalize, clamp_mag, use length_sq, barycentric, move_towards, orthonormalize, clamp_length instead.
  • Add Quaternion conversion functions to from Euler angles and axis+angle.
  • math::deg_to_rad and math::rad_to_deg respects the underlying type, returning float on a float argument.
  • Added float.to_radians and float.to_degrees and the same for double.
  • Added Quat, Mat2, Mat3 and Mat4, Vec2, Vec3, Vec4 aliases.
  • Added is_normalized to Quaternion and floating point vectors.
  • Added quaternion::from_rotation and quaternion::from_normalized_rotation
  • Added Rect type.
  • Added matrix::frustum.
  • Added math::@abs for compile time abs.
  • Make Errno a constdef containing all definitions. Deprecated libc::errno constants.
  • random::seeder no longer uses temp memory.
  • Add simple member-wise struct comparison with member_eq. #2801
  • std::core::mem::allocator deprecated and split into std::core::mem::allocators containing allocators and std::core::mem::alloc for various allocation methods.
  • Add always_assert builtin macro.
  • Add an entropy module to generate cryptographically-secure random bytes. #3022
  • Add a builtin TIMEOUT fault definition. #3022
  • Base32, Base64, Hex and Codepage encoding deprecates encode_buffer and decode_buffer. Those are replaced by encode_into and decode_into with dst being the first argument. #3055
  • hex::encode_bytes and hex::decode_bytes are deprecated in favour of hex::encode_bytes_into and hex::decode_bytes_into which has dst the first argument. #3055
  • Deprecation of @unaligned_load and @unaligned_store. Use mem::load and mem::store instead.

Fixes

  • @deprecated in function contracts would be processed twice, causing a compilation error despite being correct.
  • Name conflict with auto-imported std::core, but it should have lower priority #2902
  • Regression: missing generic nesting check on non-types.
  • Improved stringify.
  • PollSubscribe was incorrectly an int instead of ushort. #2997
  • SubProcessOptions.search_user_path did nothing on non-windows systems despite comment saying it should #2845
  • AES implementation fixed to be constant time #2806
  • Object would not properly compile on 32-bit Linux.
  • read_varint and write_varint did not work properly for ulong and wider.
  • io::EOF.nameof would yield just EOF whereas resolving it at runtime would (correctly) yield io::EOF.
  • $stringify would incorrectly capture lambdas. #2986
  • Regression: String was not implicitly @constinit #2983
  • Compiler does not propagate @noreturn through macros using short declaration syntax #3011
  • Debug info emitted on -Os #3015
  • @assert_leak() would not work properly with --safe=no #3012.
  • Duplicate symbols when building executables on Termux. #2984
  • double[<*>].max and .min were broken.
  • Incorrect codegen, crashing the compiler, when passing a { .xy = 1 } constant initializer vector to a function taking a vector, hitting vec->array conversion. #3035
  • Folding an anon struct member at compile time would crash #3034.
  • Crash in sema_compare_weak_decl when replacing a function declaration from a .c3i file in some cases #3031
  • Issue with 'inline' keyword on enum and constdef #3032.
  • When checking aliases alias FOO = _BAR the compiler would incorrectly would say that _BAR wasn't a constant.
  • Wasm32 builds crash on startup (unreachable!) due to atexit signature mismatch #3040
  • @nodiscard, @maydiscard and @noreturn weren't properly handled for function type declarations.
  • $defined with body expansion would not correctly check if parameters were the right type.
  • mask_from_int would miscompile on some platforms.
  • Overaligning structs while using @packed would cause incorrect lowering #3000
  • Splatting a literal into a typed vaarg, e.g. test(...(int[2]){ 88, 99 }, a: 123) could cause the compiler to crash.
  • &some_global[0] was incorrectly considered a global constant when some_global was a slice.
  • Taking the type of a macro identifier would give the wrong error.
  • Taking the type of a $$builtin function would crash the compiler.
  • Wrong error message when trying to take the address of a $$builtin function.
  • Accessing a (non-existing) property on a type-call would crash the compiler.
  • Crash instead of error when having two vaargs and the last one is an untyped vaarg.
  • Detect recursive declaration int[type()] type.
  • Compiler would not propagate error when $$str_find or $$str_hash arguments were invalid, causing a crash.
  • Error on wrong expression when the slice range start is out of range.
  • void{} would be looked up as generic in some cases and cause a crash.
  • Inferring generic parameters recursively would fail to construct a valid source location and crash.
  • Comparing an array of function pointers with any other type could crash rather than being an error.
  • Crashing on codegen if an internal fault in if-catch is guaranteed to bypass the conditional.
  • In $foreach in some cases the elements was an untyped variable which would cause a crash.
  • Creating a global slice would be runtime checked for null in some cases.
  • @ensure and @require could contain rethrows, which then would crash the compiler.
  • Crash when using $defined($Type) and $Type is a typeid.
  • Assigning to a subscripted const like {1, 2}[n] = 33 wasn't marked as an error and resulted in a crash.
  • Codegen for the case when an assert always panics would cause a crash.
  • Lambda check might run against a missing type definition if the function type alias was invalid.
  • Missing check when doing $foo++ would crash the compiler if the variable wasn't initialized.
  • Incorrect handling of attribute operator symbols could crash the compiler instead of producing an error.
  • Crash instead of error when the first method parameter is a vaarg.
  • Fixes to UnalignedRef.
  • Codegen would not pop debug location for a never-entered for loop, crashing LLVM lowering.
  • Double negating a vector would cause a crash in lowering.
  • Combining operator overload on a variadic method would cause a crash rather than emitting an error in some cases.
  • Lambdas as default arguments were tagged with the wrong module, leading to linking issues.
  • An initializer list with an optional field was incorrectly considered constant.
  • Fix in ringbuffer for the case of popping at position 0.

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.

Discuss this article on Reddit or Hacker News.

C3 0.7.10 - Constdef finally takes shape

After the big enhancement for generics in 0.7.9, coupled with the large number of bug fixes, 0.7.10 is naturally a more modest improvement.

A big change with the 0.7.10 release is that it's built with custom LLVM builds. This allows us to compile without unnecessary dependencies (the c3c binaries infamously depended on libxml2.so.2 due to the LLVM.org precompiled static libraries needing it).

The major change in 0.7.10 is resolving the fate of "const enums", but it also makes some other quality-of-life improvements to the language.

Constdef

Very early on C3 made the change from C-style "enum is a number" to a strictly ordinal backed enum. What this means, is that there are no gaps in C3 enums.

If we look at this enum in C, it's perfectly representable in C3:

enum Foo
{
    ABC,
    DEF,
    GHI,
    JKL
};

The corresponding C3 enum would simply be:

enum Foo
{
    ABC,
    DEF,
    GHI,
    JKL
}

However, if we had the following C enum, C3 regular enums would not be able to match it:

enum Foo
{
    ABC,
    DEF = 3,
    GHI,
    JKL
};

Because now the enum would have a gap, missing the ordinals 1 and 2.

The 1:1 mapping between C3 enums and integers allows C3 enums to support name lookups and associated values without overhead:

enum Greet : int (String fmt, String country)
{
    HELLO { "Hello %s", "USA" },
    NIHAO { "%s, 你好", "Taiwan"},
    HEJ { "Hej %s", "Sweden" }
}

fn void greet(Greet g, String name)
{
    io::printn("Using %s for greeting in %s.", g.nameof, g.country);
    io::printfn(g.fmt, name);
}

fn void test()
{
    // Prints
    // "Using HEJ for greeting in Sweden."
    // "Hej Sven"    
    greet(HEJ, "Sven");
    // Prints
    // "Using NIHAO for greeting in Taiwan"
    // "小龍, 妳好"
    greet(NIHAO, "小龍");
}

In this example, the lookup of .nameof, .country and .fmt is just indexing into an array.

If this had been C, we'd instead have to maintain such an array manually, alternatively solve it with a switch statement:

// C equivalent of .country
const char *get_country(enum Greet g)
{
    switch (g) {
        case GREET_HELLO: return "USA";
        case GREET_NIHAO: return "Taiwan";
        case GREET_HEJ: return "Sweden";
        default: return "Unknown country"
    }
}

This need for manual maintenance often leads to bugs, often in the worst possible situation - like when printing to an error log.

So the problems C3's enum is addressing are important to fix, but there is a problem – what about the situation when you need enums with gaps?

Enums as groups of constants

C's enums conflate two things: (1) a closed set of values mapping 1:1 to an underlying value (2) a grouped set of constants with a distinct type.

A classic variant of (2) in C is defining masks:

typedef enum 
{
    MASK_ABC = 1 << 0,
    MASK_DEF = 1 << 1,
    MASK_GHI = 1 << 2
} Mask;    

Mask start = MASK_ABC | MASK_GHI;

In this case there is no intention to go back and forth from value to enum, since values do not map to a single value. Our MASK_ABC | MASK_GHI has the value 5, which doesn't match any of the defined enum values.

The C usage is fine, but enums defined in this way can certainly not have an array or switch lookup to find the name, because there is not even a single name to match on!

Other usages are matching on binary protocols, where the underlying value of each enum value is language-independent. Furthermore, long-term some values may get deprecated and later completely removed.

This type of enum is not a closed set of values, but rather an open set of related constants.

In C, these two are conflated, but that approach is difficult in C3.

Emulating C enums

The first approach tried in C3 was to create a distinct type with a custom submodule:

// C version
typedef enum {
    FLAG_VSYNC_HINT         = 0x00000040, 
    FLAG_FULLSCREEN_MODE    = 0x00000002, 
    FLAG_WINDOW_RESIZABLE   = 0x00000004,
    ...
} ConfigFlags;

// Usage
ConfigFlags flags = FLAG_VSYNC_HINT | FLAG_FULLSCREEN_MODE;
// Early C3 version
module raylib;
...
typedef ConfigFlags = int;
// Submodule contains the constants
module raylib::config_flags;
const VSYNC_HINT         = 0x00000040; 
const FULLSCREEN_MODE    = 0x00000002; 
const WINDOW_RESIZABLE   = 0x00000004;

module my_game;
import raylib;
// Usage
ConfigFlags flags = config_flags::VSYNC_HINT 
                    | config_flags::FULLSCREEN_MODE;

As we can see, usage is very similar, and we get the same distinct type as in C, but there's a significant amount of song and dance to create the config flags.

There was a lot of experimentation in early versions of 0.7.x to allow regular enums to "convert" to integers, this culminated in this functionality:

// C3 inline enum values
module raylib;

typedef ConfigFlagVal = int;
enum ConfigFlags : int(inline ConfigFlagVal val)
{
    VSYNC_HINT         = 0x00000040, 
    FULLSCREEN_MODE    = 0x00000002, 
    WINDOW_RESIZABLE   = 0x00000004,
}

module my_game;
import raylib;
// Usage
ConfigFlagVal flags = ConfigFlags.VSYNC_HINT 
                      | ConfigFlags.FULLSCREEN_MODE;
// Implicitly what happens is
// ConfigFlagVal flags = ConfigFlags.VSYNC_HINT.val
//                       | ConfigFlags.FULLSCREEN_MODE.val;

However, this was deemed way to fiddly and advanced to do, which eventually led to capitulation and the const enums were introduced in 0.7.4:

// C3 const enums
module raylib;

// "const" creates a const enum
enum ConfigFlags : const int
{
    VSYNC_HINT         = 0x00000040, 
    FULLSCREEN_MODE    = 0x00000002, 
    WINDOW_RESIZABLE   = 0x00000004,
}

module my_game;
import raylib;
// Usage
ConfigFlags flags = ConfigFlags.VSYNC_HINT 
                    | ConfigFlags.FULLSCREEN_MODE;

Now we're basically having C enums. But there is something not so nice with this:

enum Foo : const int
{
    ABC,
    DEF
}
enum Bar : int
{
    ABC,
    DEF
}

fn void test()
{
    io::printn(Foo.DEF); // Prints 1
    io::printn(Bar.DEF); // Prints DEF
}

Enter "constdef"

The C enums were quite different, but shared basically all visual similarities. So it was decided to rename these, there were alternatives such as these:

const enum Foo
{
    ABC,
    DEF
}
cenum Foo
{
    ABC,
    DEF
}
enumc Foo
{
    ABC,
    DEF
}

During this discussion, it was revealed that a lot of people just defaulted to enum Foo : const int. Even just calling them const enum got people to think they were essentially the same as the regular enums. Some people even expressed the opinion that the const enums should be the default enums.

This showed how the problem had become one of communication: by sharing a similar name, people assumed the same functionality. It just seemed that "const enums" were just like regular enums but "better", because they appeared to give more options, even though as we saw above, const enums weren't able to provide any of the useful features of the regular C3 enums.

A major break with the old syntax was needed. So rather than the conservative "const enum" or "cenum", the name constdef was chosen. Since faultdef, typedef and attrdef was already established syntax, that name was in line with other C3 keywords.

So the 0.7.10 version of the example becomes

// C3 constdef
module raylib;

constdef ConfigFlags : int
{
    VSYNC_HINT         = 0x00000040, 
    FULLSCREEN_MODE    = 0x00000002, 
    WINDOW_RESIZABLE   = 0x00000004,
}

module my_game;
import raylib;
// Usage
ConfigFlags flags = ConfigFlags.VSYNC_HINT 
                    | ConfigFlags.FULLSCREEN_MODE;

So semantically we're using the enum Foo : const int of 0.7.4, but with a name that clearly indicates its capabilities. In essence it is not very different from the approach of typedef + const that we started with, but it's much cleaner.

The change in name makes the trade-off obvious: pick enum and you can't change the underlying value, pick constdef and you don't get runtime name reflection.

constdef goes further than C enums though. It may be any value, not just an integer. So this is just as valid:

constdef GreetingFmt : String
{
    HELLO = "Hello %s",
    NIHAO = "%s 你好",
    HEJ = "HEJ %s",
}

This would be just the same as defining the constants:

typedef GreetingFmt = String;
const GreetingFmt HELLO = "Hello %s";
const GreetingFmt NIHAO = "%s 你好";
const GreetingFmt HEJ = "HEJ %s";

So this extends the C idea of "enums as a distinct set of constants" from just integers to any type.

To further make enums and constdefs distinct, the syntax from the inline experiment have been reverted, and to declare associated values for enums {} is now used:

// Pre 0.7.10
enum Foo : (int a)
{
    ABC = 2,
    BCD = 3,
}
enum Bar : (int a, String b)
{
    TEST1 = { 1, "a" },
    TEST2 = { 75, "foo" }
}
// 0.7.10
enum Foo : (int a)
{
    ABC { 2 },
    BCD { 3 },
}
enum Bar : (int a, String b)
{
    TEST1 { 1, "a" },
    TEST2 { 75, "foo" }
}

Compatibility with 0.7.9 and earlier

As per usual, the "const enum" syntax will still work until 0.8.0. Likewise, the old style of declaring associated values for regular enums will keep working. By default, a deprecation notice will be shown, but this can be suppressed by using --warn-deprecation=no as a command line option.

Typedef literal conversion changes

In 0.7.9 and before, distinct types defined with typedef would implicitly convert from any literal or constant value. To avoid this behaviour you needed to add @structlike. However, it's been established that this is the wrong default. With 0.7.10 the default is swapped up: use @constinit to allow implicit casts from constants and by default it's not supported.

However, for backwards compatibility the old behaviour will only yield a deprecation notice.

// 0.7.9 behaviour
typedef MyNumber = int;
typedef Temperature @structlike = int;

MyNumber n = 0;
// Temperature t = 0; Error: needs explicit conversion
Temperature t = (Temperature)0; 

// 0.7.10
typedef MyNumber @constinit = int;
typedef Temperature = int;

MyNumber n = 0;
// Temperature t = 0; Error: deprecated
Temperature t = (Temperature)0;

Method resolution and $defined

A problem has been using $defined with methods, since methods are associated with their underlying types fairly late. For this reason 0.7.9 tightened the constraints as to when it was possible to test a method.

Here's a problematic example where the dependency is circular:

struct Foo { int a; }

fn void Foo.test(&self) @if($defined(Foo.test2))
{}
fn void Foo.test2(&self) @if($defined(Foo.test))
{}

While it's important to detect these kinds of circular dependencies, 0.7.9 would also end up disallowing well-ordered used of $defined with methods.

This changes in 0.7.10. Rather than checking if the method resolution is complete before $defined is invoked, the compiler will "tag" the parent type when its methods are referenced in a $defined. If a method is later added to the parent type, a warning will be issued.

So the check is now tied to whether there is legitimate ambiguity to the $defined, rather than assuming it is wrong if it's invoked out of order.

This model is easy to implement and also greatly improves on the original check.

Integrated MSVC SDK download

C3's long had the ability to cross-compile to MSVC, but now at long last this is no longer supported using a separate script, but everything is integrated into the C3 compiler yielding a silky smooth experience whether just building on Windows or cross-compiling.

Semantics changes

C3's unsigned % signed and unsigned / signed conversions would typically convert the unsigned part to signed. While this is reasonable for other arithmetics, it leads to very surprising behaviour for division/remainder. Since the cases where this happened was very likely to yield buggy behaviour, this is now a hard error.

Cases like unsigned % 1 changes to that the denominator is turned into an unsigned value.

Examples:

int y = 2;
uint x = uint.max / y;  // Invalid, requires explicit cast.
uint y = uint.max / 2;  // Implicitly converts denominator to 2U.
uint z = uint.max / -2; // Invalid, requires explicit cast.  

Warnings

While the C3 compiler has had settings for silencing/enabling warnings through --validation and --silence-deprecation, it didn't have any uniform system for warnings. 0.7.10 changes this and adds --warn-* family of custom settings for individual warnings. Expect this to be expanded on in future versions of the compiler.

Method visibility warnings

Method visibility has been ignored since 0.7.0, but no warning has been issued. Now these warnings have been added, and consequently many stdlib methods have been updated as a result.

struct Foo { int a; }

// @local is ignored, this is a warning in 0.7.10
fn int Foo.test(self) @local
{
    return self.a;
}

fn int Foo.do_something(self)
{
    return self.test();
}

If you were relying on hiding implementation details with local or private methods, use a local / private function instead.

// Using a @local function instead:
fn int _test(Foo self) @local
{
    return self.a;
}

fn int Foo.do_something(self)
{
    return _test(self);
}

Tooling improvements

Android Termux support: has been improved and should now work properly.

Library support: c3c init for libraries now provides helpful examples of exported functions.

Improved Vendor Fetch: c3c vendor-fetch now helpfully lists all packages available from vendor.

Tracking inlining and function sizes: The --print-large-functions has been added. This commandline switch will print out the names of functions that have a large number of instructions. If a seemingly normal function has a large number of instructions, then this signals that the function is likely using too much macro inlining. Aside from longer compile times and larger binary sizes, this will affect the instruction cache, potentially yielding worse performance despite inlining.

'@deprecated' as a contract directive

@deprecated has been available as a contract directive, but it didn't do anything. Starting with 0.7.10 this works properly.

// Deprecation using attribute:
<* 
 Call this old function
*>   
fn void old_test() @deprecated("use new_test")
{ ... }
// Deprecation using contract

<* 
 Call this old function
 @deprecated "use new_test"
*>   
fn void old_test()
{ ... }

Stdlib updates

  • PEM encoding / decoding
  • New hash implementations: Murmur3 and Xorshiro128++
  • Optional line-length cutoff parameter in io::readline
  • array::even, array::odd and array::unlace array filtering functions.
  • Single-byte code page support (DOS/OEM, Windows/ANSI, and ISO/IEC 8859)
  • Discrete and continuous distributions added to std::math

Changes in the stream API

The original stream API used isz and usz for seek and available functions. This has been updated to use 64-bit ints on all platforms. This solves issues working with large files on 32-bit systems.

As part of this, InStream.seek is replaced by set_cursor and cursor.

Notable fixes

  • --cpu-flags didn't work if the first item was an exclusion.
  • Reallocating overaligned memory with the LibcAllocator was unsafe.
  • std::io::Formatter would print incorrect values for values exceeding int128.max.
  • --safe=no would accidentally disable compile-time error reporting on compile-time known runtime @require checks.
  • Member access on a struct returned by an assignment expression, e.g. (foo = bar()).a would cause a crash.

Looking Forward

0.7.11 should bring a healthy number of additions to the stdlib, and there needs to be some early preparation for 0.8.0 as well.

The surrounding tooling is what needs the most attention:

  • Evolving beyond vendor-fetch for retrieving libraries.
  • The need for a SOLID LSP is getting more urgent.
  • An official C3 code formatter is needed.
  • Likewise, an official C3 docgen is getting increasingly urgent.

Community and Contributions

This release wouldn't have been possible without the C3 community. I'd like to extend a deep thank you to all who have contributed, both through filed issues, PRs and just plain discussions.

PR contributors for this release:

Stdlib: Book-reader, Fernando López Guevara, konimarti, Laura Kirsch, Manu Linares, mmoustafa8108, soerlemans, Zack Puhl.

Compiler & toolchain: Book-reader, Damien Wilson, Foxy-Boxes, Gantsev Denis, Kiana, Laura Kirsch, Lucas Alves, Manu Linares, Samuel, srkkov

CI/Infrastructure: Manu Linares, Rauny, Smite Rust.

Change Log

Click for full change log

Changes / improvements

  • C3 is now using its own LLVM libraries when building releases.
  • Method resolution and $defined now works together well unless definitions are out of order for real.
  • Improve error message when using functions as values #2856
  • Improve support for Android with Termux.
  • Integrated download of the MSVC SDK when compiling for Windows.
  • For c3c init with library templates, provide example exported functions. #2898
  • unsigned % signed and unsigned / signed is no longer allowed without explicit casts, except for const denominators. #2928
  • New enum associated value syntax.
  • Individual warning settings added.
  • Change typedef and const enums to not convert from literals by default.
  • Add @constinit to allow old typedef behaviour.
  • Include actual element count in the error message when the array initializer size does not match the expected size.
  • Add --print-large-functions for checking which functions likely dominate the compile time.
  • Improve error message when providing alias with a typeid expression where a type was expected. #2944
  • Const enums removed.
  • Constdef declarations introduced.
  • Properly support @deprecated as contract.
  • Support deprecating enum values.
  • Improve error when trying to use an extern const as a compile time constant. #2969
  • vendor-fetch command now lists all available packages by default. #2976
  • Typekind enums are changed CONST_ENUM -> CONSTDEF, DISTINCT -> TYPEDEF.

Stdlib changes

  • Summarize sort macros as generic function wrappers to reduce the amount of generated code. #2831
  • Remove dependency on temp allocator in String.join.
  • Remove dependency on temp allocator in File.open.
  • Added PEM encoding/decoding. #2858
  • Add Murmur3 hash.
  • Add optional line-length limitations to io::readline and io::readline_to_stream. #2879
  • Add Xorshiro128++.
  • Add single-byte code page support (DOS/OEM, Windows/ANSI, and ISO/IEC 8859).
  • Add array::even, array::odd, and array::unlace macros. #2892
  • Add discrete and continuous distributions in std::math::distributions.
  • Add bitorder functions store_le, load_le, store_be, store_le.
  • Stream functions now use long/ulong rather than isz/usz for seek/available.
  • instream.seek is replaced by set_cursor and cursor.
  • instream.available, cursor etc are long/ulong rather than isz/usz to be correct on 32-bit.
  • Enable asynchronous, non-blocking reads of subprocess STDOUT/STDERR pipes on POSIX systems.

Fixes

  • Add error message if directory with output file name already exists
  • Regression where nested lambdas would be evaluated twice.
  • Compiler crash when using arrays of vectors in lists. #2889
  • Fix list[0].i = 5 when list[0] returns a pointer. #2888
  • Shadowing not detected for generic declarations #2876
  • Const inline enums would not always implicitly get converted to the underlying type.
  • Update to dstring.append_string to take any type converting to String.
  • Flag --cpu-flags doesn't work if the first item is an exclusion. #2905
  • Reallocating overaligned memory with the LibcAllocator was unsafe.
  • Using [] or .foo on $$ functions would not raise error but instead crash
  • Improved underlining errors/warnings when unicode is used. #2887
  • Fix std::io::Formatter integer issue for large uint128 decimal values.
  • --safe=no disabled compile-time errors on compile-time known runtime @require checks #2936
  • On assert known false, the message was not shown for no-args.
  • Adding the incorrect sized vector to a pointer vector would cause a crash.
  • Member access on a struct returned by the assignment expression, cause crash #2947
  • Trying to slice an indexable type leads to misleading error message #2958
  • Warn on use of visibility modifiers on methods. #2962
  • Compiler crash using ?? with a void? macro #2973
  • Fix issue when extending a generic type with a method in another module.

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.

Discuss this article on Reddit or Hacker News.

C3 0.7.9 - New generics and new optional syntax

0.7.9 revamps the generics, moving from a strict module-based generic module to something similar to conventional generics, but retaining the advantages of module-based generics.

New generics

C3 traditionally had a generic approach which was based on the concept of generic modules:

module list {Type};

struct List
{
    Type* data;  // Type is an alias in this module scope
    usz capacity;
    usz size;
}

fn List new_list() { ... }

When one symbol of the module is generated, all are:

List{int} a; // This also generates list::new_list{int}

This grouping allowed constraints to be placed on the module:

<*
 @require $defined((Type){} + (Type){}) : "Type must respond to +'"
*> 
module num {Type};

fn Type add_two(Type t1, Type t2)
{
    return t1 + t2;
}

fn Type add_three(Type t1, Type t2, Type t3)
{
    return t1 + t2 + t3;
}

module test;
import num;

fn void main()
{
    int i = num::add_two{int}(1, 2);
    // This next gives "Type must respond to +" as error,
    // same happens if num::add_two is instantiated instead.
    void* ptr = num::add_three{void*}(null, null, null); 
}

The advantage of this is that the generic checking can be centralized and easily inlined at the calling site. However, having to create a new module for every generic is very heavy-weight, and increased dependency on macros.

Group-based generics

Rather than setting the unit to be the module, the new generic system has module groups. A module declaration in the same module with the same argument name/names are considered belonging to the same group. The parameters are declared after the module or declaration.

module some_module;
fn Type foo(Type t) <Type> // Group 1
{
    return t * 2;
}

fn Type bar(Type t1, Type t2) <Type> // Group 1
{
    return t1 * t2
}

fn OtherType baz(OtherType t1, OtherType t2) <OtherType> // Group 2
{
    return t1 / t2;
}

Only all declarations in the same group are instantiated together:

foo{int}(1); // Instantiates foo and bar, not baz
baz({double}(1.3, 0.5); // Instantiates baz only

Adding it to the module declaration, makes all declarations in the module section marked with that group, making it compatible with old module-based generics:

module my_generic <Type>;

struct Foo  // Implicitly <Type>
{
    Type a, b;
}
fn Type test() => {}; // Implicitly <Type>

module other;
import my_generic;

Foo{int} x;
fn main()
{
    Foo{int} y = my_generic::test{int}();
}

Combining constraints

Constraints will be shared between declarations in the same group, except for function and macro declarations:

module test;

<* @require $Type.kindof == SIGNED_INT : "Must be a signed integer" *>
struct Foo <Type>
{
    Type a, b;
}
<* require $Type.sizeof >= 4 : "Must be at least 32 bits" *>
const Foo BAZ <Type> = { 1, 2 };

fn void test()
{
    Foo{int} i; // Works
    Foo{short} s; // Error: "Must be at least 32 bits"
    Foo{double} s; // Error: "Must be a signed integer"
}

Generic inference

Generics don't do general inference (although some improvements might happen in later versions). However, in assigment it will infer to the same parameters as the assigned-to expression used.

module test;

struct Foo <Type>
{
    Type a, b;
}
fn Type test() <Type> => {};

fn main()
{
    Foo{int} y = test(); // inferred to be test{int}
}

Further refinement

Aside from the more lightweight use, the new generics feature allows migration from code that today may use macros. As a later improvement, inference may get better to allow more use without explicit arguments. On top of this, it could later allow things like generics taking typed constants.

New Optional syntax

Previous to 0.7.9 the following held:

int? y = 1; // Asign a regular value
int? x = io::EOF?; // Assign the excuse io::EOF to, making it an Empty optional
int z1 = x!; // Rethrow if x is Empty
int z2 = x!!; // Panic if x is Empty
int z3 = x ?? 3; // Return 3 if x is Empty

Unfortunately the suffix ? requires a very roundabout grammar, with very special handling to avoid conflict with ternary ?. During the development of 0.7.9 we tested a lot of alternatives, such as ^io::EOF and ?io::EOF, but finally it was decided that suffix ~ was the least bad.

So starting from 0.7.9 suffix ? is deprecated and replaced by ~:

int? x = io::EOF~;

This also affects the two so-called "nani" pseudo-operators ?! and ?!!:

// Before:
io::EOF?!;  // Create io::EOF and immediately rethrow it
io::EOF?!!; // Create io::EOF and immediately panic

// After – goodbye "nani" 😭
io::EOF~!;
io::EOF~!!;

Note that the change only affects turning a fault into an Optional. Type declarations still use ? suffix to indicate an optional type.

Extended platform support

Custom Libc

--custom-libc is a new alternative to libc/no-libc, which allows the stdlib to work as if a regular libc is available but doesn't automatically link to a particular libc. This can be used to provide replacement libc implementations.

Support for NetBSD

Improvements have been made for NetBSD support as a target.

Changes to reflection

The shorthand foo.$abc for foo.eval("$abc") has been introduced:

// Before:
macro get_field(v, String $fieldname)
{
    return v.$eval($fieldname);
}
// After:
macro get_field(v, String $fieldname)
{
    return v.$fieldname;
}

New builtins

$$int_to_mask and $$mask_to_int efficiently converts from an integer into a vector bool bitmask and back.

They're available as math::int_to_mask(VectorType, val) and bool_vector.mask_to_int().

In addition to this, $$unaligned_load and $$unaligned_store now also takes an "is_volatile" parameter, allowing for volatile unaligned loads.

New compile time constants are also available: $$VERSION and $$PRERELEASE, which return the compiler version and whether it is a prerelease version or not.

Windows improvements

On win32 utf-8 console output is now enabled by default in compiled programs.

Deprecated multi-level array length inference

Because of the complexity to implement it, array length inference where anything other than the top level is inferred (e.g. int[*][*] or int[*]*) is deprecated.

There were a lot of bugs with this feature, and the complexity simply was not worth the very rare use case. Note here that C doesn't support it either.

Fixes

Thanks to the hard work of contributors exploring corner-cases plus the uncommonly long development time, this version contains over 120 fixes(!).

Stdlib changes

Deprecations

Several functions relating to threads no longer need to be checked for the return value, and will return void in 0.8.0: Mutex's destroy(), unlock() and lock(), Thread's destroy(), join() and detach(), ConditionVariable's destroy(), signal(), broadcast() and wait(). Using these wrong are instead contract violations.

For DString.append_chars is deprecated: use DString.append_string instead. Appending a dstring should use DString.append_dstring instead.

Using EMPTY_MACRO_SLOT has been deprecated as well, since there are good replacements for it.

Crypto and hashing

This version adds: Poly1305, Ripemd, Chacha20, Blake2, Blake3 and streebog.

New functions for unaligned load and store

mem::store and mem::load has been added, which mostly replace existing macros for volatile and unaligned load/store. In addition to this they allow unaligned and volatile access to be combined.

Assorted additions

@in macro checks a constant in a list at compile time. ThreadPool has join() to wait for all threads without destroying the threads in the pool. any.to and any.as methods corresponding to anycast. The ansi module adds a struct to print arbitrary ansi RGB values to a formatter without allocation. allocator gains realloc_array functions for reallocating an array created using alloc_array.

Looking Forward

For 0.7.10, there are already some really nice additions in the PR queue to improve tooling. The generics can be further refined, which will also likely happen this next release. Compile time evaluation order – which now changed a bit with the new generics, needs to be rewritten a bit in the next cycle as well.

And finally we hope C3 will finally rely on its own LLVM library builds for Linux and MacOS with 0.7.10.

Community and Contributions

This release wouldn't have been possible without the C3 community. I'd like to extend a deep thank you to all who have contributed, both through filed issues, PRs and just plain discussions.

Change Log

Click for full change log

Changes / improvements

  • Add --custom-libc option for custom libc implementations.
  • Support for NetBSD.
  • Testing for the presence of methods at the top level is prohibited previous to method registration.
  • $$mask_to_int and $$int_to_mask to create bool masks from integers and back.
  • Better error messages when slicing a pointer to a slice or vector. #2681
  • Generics using ad-hoc <...> rather than module based.
  • Reduced memory usage for backtraces on Linux.
  • On win32 utf-8 console output is now enabled by default in compiled programs
  • Add $$VERSION and $$PRERELEASE compile time constants.
  • Require () around assignment in conditionals. #2716
  • $$unaligned_load and $$unaligned_store now also takes a "is_volatile" parameter.
  • Module-based generics using {} is deprecated.
  • Create optional with ~ instead of ?. return io::EOF?; becomes return io::EOF~.
  • Deprecated use of ? to create optional.
  • Make foo.$abc implicitly mean foo.eval("$abc").
  • Deprecating multi-level array length inference. int[*][*] is deprecated and will be removed 0.8.0.
  • Combining argument-less initialization with argument init for bitstructs is now allowed e.g. { .b, .c = 123 }.

Fixes

  • Remove use of LLVMGetGlobalContext for single module compilation.
  • Fixed bug where constants would get modified when slicing them. #2660
  • Regression with npot vector in struct triggering an assert #2219.
  • Casting bitstruct to wider base type should be single step #2616.
  • Optional does not play well with bit ops #2618.
  • Bytebuffer.grow was broken #2622.
  • Hex escapes like "\x80" would be incorrectly lowered. #2623
  • Ignore const null check on deref in $defined and $sizeof #2633.
  • Subscripting of constant slices would sometimes be considered non-constant #2635.
  • Compiler crash when concatenating structs and arrays to an untyped list.
  • Strings assigned to longer arrays would crash codegen, e.g. char[10] x = "abcd.
  • Typedefs and structs with inline types supporting lengthof would not work with lengthof #2641.
  • $defined(foo()) now correctly errors if foo() would require a path.
  • @if($defined((char*){}.foo())) does not error if foo is missing.
  • Hard limit of 127 characters for identifiers.
  • $$LINE would sometimes yield the incorrect format.
  • Fix error message when a method has the wrong type for the first argument.
  • Unit tests allocating too much tmem without @pool would cause errors in unrelated tests. #2654
  • Incorrect rounding for decimals in formatter in some cases. #2657
  • Incorrectly using LLVMStructType when emitting dynamic functions on MachO #2666
  • FixedThreadPool join did not work correctly.
  • Fix bug when creating bool vectors in certain cases.
  • Compiler assert when passing returning CT failure immediately rethrown #2689.
  • Converting between simd/non-simd bool vector would hit a compiler assert. #2691
  • i<n> suffixes were not caught when n < 8, causing an assert.
  • Parse error in $defined was not handled correctly, leading to an assertion.
  • Assert when struct/array size would exceed 4 GB.
  • Assert when encountering a malformed module alias.
  • Assert when encountering a test function with raw vaarg parameters.
  • foo.x was not always handled correctly when foo was optional.
  • x'1234' +++ (ichar[1]) { 'A' } would fail due to missing const folding.
  • Miscompilation: global struct with vector could generate an incorrect initializer.
  • String.tokenize_all would yield one too many empty tokens at the end.
  • String.replace no longer depends on String.split.
  • Fix the case where \u<unicode char> could crash the compiler on some platforms.
  • Designated initialization with ranges would not error on overflow by 1.
  • io::read_fully now handles unbounded streams properly.
  • Crash when doing a type property lookup for const inline enums in some cases #2717.
  • Incorrect alignment on typedef and local variable debug info.
  • Assert on optional-returning-function in a comma expression. #2722
  • Creating recursive debug info for functions could cause assertions.
  • bitorder::read and bitorder::write may fail because of unaligned access #2734.
  • Fix LinkedList.to_format to properly iterate linked list for printing.
  • Hashing a vector would not use the entire vector in some cases.
  • Fix to temp_directory on Windows #2762.
  • Too little memory reserved when printing backtrace on Darwin #2698.
  • In some cases, a type would not get implicitly converted to a typeid #2764.
  • Assert on defining a const fault enum with enumerator and fault of the same name. #2732
  • Passing a non-conststring to module attributes like @cname would trigger an assert rather than printing an error. #2771
  • Passing different types to arg 1 and 2 for $$matrix_transpose would trigger an assert. #2771
  • Zero init of optional compile time variable would crash the compiler. #2771
  • Using multiple declaration for generics in generic module would fail. #2771
  • Defining an extern const without a type would crash rather than print an error. #2771
  • Typedef followed by brace would trigger an assert. #2771
  • Union with too big member would trigger an assert. #2771
  • Bitstruct with unevaluated user-defined type would cause a crash. #2771
  • Using named parameters with builtins would cause a crash. #2771
  • In some cases, using missing identifiers with builtins would cause a crash. #2771
  • Using $defined with function call missing arguments would cause a crash. #2771
  • Adding @nostrip to a test function would crash. #2771
  • Mixing struct splat, non-named params and named params would crash rather than to print an error. #2771
  • Creating a char vector from bytes would crash. #2771
  • Using $$wstr16 with an illegal argument would crash instead of printing an error. #2771
  • Empty struct after @if processing was not detected, causing a crash instead of an error. #2771
  • Comparing an uint and int[<4>] was incorrectly assumed to be uint compared to int, causing a crash instead of an error. #2771
  • When an int[*][6] was given too few values, the compiler would assert instead of giving an error. #2771
  • Inferring length from a slice was accidentally not an error.
  • Eager evaluation of macro arguments would break inferred arrays on some platforms. #2771.
  • Vectors not converted to arrays when passed as raw vaargs. #2776
  • Second value in switch range not checked properly, causing an error on non-const values. #2777
  • Broken cast from fault to array pointer #2778.
  • $typeof untyped list crashes when trying to create typeid from it. #2779
  • Recursive constant definition not properly detected, leading to assert #2780
  • Failed to reject void compile time variables, leading to crash. #2781
  • Inferring the size of a slice with an inner inferred array using {} isn't detected as error #2783
  • Bug in sysv abi when passing union in with floats #2784
  • When a global const has invalid attributes, handling is incorrect, leading to a crash #2785.
  • int? ? was not correctly handled. #2786
  • Casting const bytes to vector with different element size was broken #2787
  • Unable to access fields of a const inline enum with an aggregate underlying type. #2802
  • Using an optional type as generic parameter was not properly caught #2799
  • Instantiating an alias of a user-defined type was not properly caught #2798
  • Too deeply nested scopes was a fatal crash and not a regular semantic error. #2796
  • Recursive definition of tag not detected with nested tag/tagof #2790
  • Attrdef eval environment lacked rtype, causing error on invalid args #2797
  • $typeof() returns typeinfo, causing errors #2795.
  • Empty ichar slice + byte concatenation hit an assert. #2789
  • Remove dependency on test tmp library for stdlib compiler tests. #2800
  • Comparing a flexible array member to another type would hit an assert. #2830
  • Underlying slice type not checked correctly in $defined #2829
  • Checking for exhaustive cases is done even in if-chain switch if all is enum #2828
  • Constant shifting incorrectly doesn't flatten the underlying vector base #2825
  • String not set as attributes resolved breaking has_tagof #2824
  • Self referencing forward resolved const enum fails to be properly detected #2823
  • Incorrectly try compile time int check on vector #2815
  • Generating typeid from function gives incorrect typeid #2816
  • Recursive definitions not discovered when initializer is access on other const #2817
  • Slice overrun detected late hit codegen assert #2822
  • Compile time dereference of a constant slice was too generous #2821
  • Constant deref of subscript had inserted checks #2818
  • Raw vaargs with optional return not lowered correctly #2819
  • Early exit in macro call crashes codegen #2820
  • Empty enums would return the values as zero sized arrays #2838
  • Store of zero in lowering did not properly handle optionals in some cases #2837
  • Bitstruct accidentally allowed other arrays than char arrays #2836
  • Bitstruct as substruct fails to properly work with designated initializers. #2827
  • Bug when initializing an inferred array with deep structure using designated init #2826
  • Packed .c3l files without compressions weren't unpacked correctly.
  • Lowering of optional in && was incorrect #2843
  • Resolving &X.b when X is a const incorrectly checked for runtime constness #2842
  • Alignment param on $$unaligned_* not checked for zero #2844
  • Fix alignment for uint128 to 16 with WASM targets.
  • Incorrect assert in struct alignment checking #2841
  • Packed structs sometimes not lowered as such.
  • Crash when creating $Type* where $Type is an optional type #2848
  • Crashes when using io::EOF~! in various unhandled places. #2848
  • Crash when trying to create a const zero untyped list #2847
  • Incorrect handling when reporting fn with optional compile time type #2862
  • Optional in initializer cause a crash #2864
  • Negating a global address with offset was a counted as a global runtime constant #2865
  • Converting static "make_slice" to array failed to be handled #2866
  • Narrowing a not expression was incorrectly handled #2867
  • Vector shift by optional scalar failed #2868
  • Initializer did not correctly handle second rethrow #2870
  • Crash encountering panic in if-else style switch #2871
  • Crash in slice expression when it contains a rethrow #2872
  • Multiple issues when rethrowing inside of expressions #2873

Stdlib changes

  • Add ThreadPool join function to wait for all threads to finish in the pool without destroying the threads.
  • Add @in compile-time macro to check for a value in a variable list of constants. #2662
  • Return of Thread/Mutex/CondVar destroy() is now "@maydiscard" and should be ignored. It will return void in 0.8.0.
  • Return of Mutex unlock() and lock() is now "@maydiscard" and should be ignored. They will return void in 0.8.0.
  • Return of ConditionVariable signal() broadcast() and wait() are now "@maydiscard". They will return void in 0.8.0.
  • Return of Thread detatch() is now "@maydiscard". It will return void in 0.8.0.
  • Buffered/UnbufferedChannel, and both ThreadPools have @maydiscard on a set of functions. They will return void in 0.8.0.
  • Pthread bindings correctly return Errno instead of CInt.
  • Return of Thread join() is now "@maydiscard".
  • Add poly1305 one-time Message Authentication Code and associated tests. #2639
  • Add ripemd hashing and associated tests. #2663
  • Add chacha20 stream cipher and associated tests. #2643
  • Add BLAKE2 (optionally keyed) cryptographic hashing with associated tests. #2648
  • Add BLAKE3 XOF and associated tests. #2667
  • Add Elf32_Shdr and Elf64_Shdr to std::os::linux.
  • Add any.to and any.as.
  • Deprecated DString.append_chars, use DString.append_string
  • Deprecated DString.append_string for DStrings, use DString.append_dstring instead.
  • Added DString.append_bytes.
  • Add streebog (aka "GOST-12") hashing with 256-bit and 512-bit outputs. #2659
  • Add unit tests for HMAC 256 based on RFC 4231. #2743
  • Add extra AsciiCharset constants and combine its related compile-time/runtime macros. #2688
  • Use a Printable struct for ansi RGB formatting instead of explicit allocation and deprecate the old method.
  • HashSet.len() now returns usz instead of int. #2740
  • Add mem::store and mem::load which may combine both aligned and volatile operations.
  • Deprecated EMPTY_MACRO_SLOT and its related uses, in favor of optional_param = ... named macro arguments. #2805
  • Add tracking of peak memory usage in the tracking allocator.
  • Added realloc_array, realloc_array_aligned, and realloc_array_try to allocator::. #2760

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.

Discuss this article on Hacker News or Reddit.

Jingle bells, C3 0.7.8

With Christmas on the horizon, C3 gets another monthly update to 0.7 with 0.7.8. As usual it brings a set of small tweaks and fixes. Let's see what we got:

Struct splatting

0.7.7 added struct initializer splatting, but it was a special case. And while it has been possible to splat an array or slice into both initializers and calls, structs didn't support that. This has been amended in 0.7.8:

struct Foo
{
    int a;
    double b;
}

fn void test(int x, double y, int a)
{
    io::printfn("%s %s", x * a, y * a);
}

fn int main()
{
    Foo f = { 42, 3.14 };
    test(...f, 2);          // prints 84 6.280000
    return 0;
}

Swizzle initialization for vectors

While vectors can use names to reference the first four values, e.g. foo.x as well as supporting swizzling: foo.xy, designated initialization has been limited to using the same syntax as arrays: { [0..1] = 1.2, [2] = 3.2 }.

With this improvement, it's now possible to use the name of the components directly:

float[<3>] x = { .xy = 1.2, .z = 3.3 };
// Same as float[<3>] x = { [0..1] = 1.2, [2] = 3.2 };
A limitation is that any swizzling syntax, like .xy must indicate a consecutive gapless range, so initializing using for example .zx or .xz would not be allowed.

Function referencing in @return? for simplified fault declarations

Before we had this:

faultdef BAD_ZERO, BAD_ONE, TOO_BIG;
<*
 @return? BAD_ZERO, BAD_ONE
*> 
fn int? foo(int a)
{
    switch (a)
    {
        case 0: return BAD_ZERO?;
        case 1: return BAD_ONE?;
        default: return a * 2;  
    }
}

// We must repeat the errors of "foo"
<*
 @return? BAD_ZERO, BAD_ONE, TOO_BIG
*> 
fn int? bar(int a)
{
    if (a > 100) return TOO_BIG?;
    return foo(a) ^ 12;
}

With this improvement, we can refer to the errors of a function (or function pointer) in the @return? statements:

<*
 @return? foo!, TOO_BIG
*> 
fn int? bar(int a)
{
    if (a > 100) return TOO_BIG?;
    return foo(a) ^ 12;
}

Enums now support membersof to return associated values.

Enums used to only support the type property .associated which returned a list of the associated value types. Enums now instead use .membersof, which works like the same property on structs. .associated has been deprecated as .membersof includes its information.

enum Foo : (String x, int val)
{
    ABC = { "Hello", 3 },
    DEF = { "World", -100 },
}

fn int main()
{
    io::printn(Foo.membersof[0].get(Foo.ABC)); // Print "Hello"
    io::printn(Foo.membersof[1].get(Foo.DEF)); // Print -100
    $assert Foo.membersof[0].type == String.typeid;
    io::printn(Foo.membersof[0].nameof);       // prints x
    Foo f = ABC;
    io::printn(Foo.membersof[1].get(f));       // prints 3
    return 0;
}

@param directives for ... parameters

C vaargs on functions were previously not possible to reference using @param. This has been improved, allowing "..." to be referenced:

<* 
 @param fmt : "the format string"
 @param ... : "the arguments to print" 
*>
extern fn int printf(ZString fmt, ...);

Missing imports allowed if module @if evaluates to false

This change means that if you add imports to a module that isn't enabled, they are not checked:

module foo @if(false);
import non_existing_lib; // Missing module

fn int test()
{
    return 0;
}

Prior to 0.7.8 this would be reported as an error due to non_existing_lib not being a valid module, but from 0.7.8 such errors are only reported if the importing module is enabled. In this example, changing @if(false) to @if(true) would make the import reported as an error.

Linux musl support

A --linux-libc command line option has been added, supporting gnu and musl options. This is the beginning of official musl support for the C3C compiler, contributed by DylanDoesProgramming.

Support of int $foo... arguments

Named macro vaargs were incorrectly handled prior to 0.7.7, but the change in 0.7.7 inadvertently prevented typed const vaargs like int $foo.... This is now enabled again.

Small bag of improvements

  • String merging of "foo" "bar" is now much more efficient, handling very long strings easily.
  • Win32 got a default exception handler thanks to TechnicalFowl.
  • $schema was added as a key in project.json.
  • The @simd implementation was changed, and @simd is now possible to use directly after the type as needed.
  • --test-nocapture is deprecated in favour of --test-show-output.
  • Xtensa target no longer enabled by default on LLVM 22, use -DXTENSA_ENABLE to enable it instead.

Fixes

0.7.8 contains around 30 fixes, with the increase compared to 0.7.7 mostly depending on the vector ABI changes which yielded some regressions to clean up in 0.7.8.

Stdlib changes

The MacOS bindings in std::os::macos nicely got a bunch of additions contributed by Glenn Kirk, and printing typeids now prints the actual underlying id as well. Printing BigInts was optimized and printf now has caching which makes printing on Win32 faster.

Looking Forward

Several things are in the pipe: possibly updating the syntax for turning a fault into an optional, going from return io::EOF?; to some syntax that makes the grammar simpler. Inline asm is still waiting for its revision, and there should be a review of the casting rules. Finally, generating proper headers when building static and dynamic is rather overdue.

Community and Contributions

This release wouldn't have been possible without the C3 community. I'd like to extend a deep thank you to all who have contributed, both through filed issues, PRs and just plain discussions.

Change Log

Click for full change log

Changes / improvements

  • Improve multiline string parser inside compiler #2552.
  • Missing imports allowed if module @if evaluates to false #2251.
  • Add default exception handler to Win32 #2557.
  • Accept "$schema" as key in project.json #2554.
  • Function referencing in @return? for simplified fault declarations. Check @return? eagerly #2340.
  • Enums now work with membersof to return the associated values. #2571
  • Deprecated SomeEnum.associated in favour of SomeEnum.membersof
  • Refactored @simd implementation.
  • Improve error message for Foo{} when Foo is not a generic type #2574.
  • Support @param directives for ... parameters. #2578
  • Allow splatting of structs. #2555
  • Deprecate --test-nocapture in favour of --test-show-output #2588.
  • Xtensa target no longer enabled by default on LLVM 22, Compile with -DXTENSA_ENABLE to enable it instead
  • Add float[<3>] x = { .xy = 1.2, .z = 3.3 } swizzle initialization for vectors. #2599
  • Support int $foo... arguments. #2601
  • Add musl support with --linux-libc=musl.

Fixes

  • Foo.is_eq would return false if the type was a typedef and had an overload, but the underlying type was not comparable.
  • Remove division-by-zero checks for floating point in safe mode #2556.
  • Fix division-by-zero checks on a /= 0 and b /= 0f #2558.
  • Fix fmod a %= 0f.
  • Regression vector ABI: initializing a struct containing a NPOT vector with a constant value would crash LLVM. #2559
  • Error message with hashmap shows "mangled" name instead of original #2562.
  • Passing a compile time type implicitly converted to a typeid would crash instead of producing an error. #2568
  • Compiler assert with const enum based on vector #2566
  • Fix to Path handling c:\foo and \home parent. #2569
  • Fix appending to c:\ or \ #2569.
  • When encountering a foreach over a ZString* it would not properly emit a compilation error, but hit an assert #2573.
  • Casting a distinct type based on a pointer to an any would accidentally be permitted. #2575
  • overflow_* vector ops now correctly return a bool vector.
  • Regression vector ABI: npot vectors would load incorrectly from pointers and other things. #2576
  • Using defer catch with a (void), would cause an assertion. #2580
  • Fix decl attribute in the wrong place causing an assertion. #2581
  • Passing a single value to @wasm would ignore the renaming.
  • *(int*)1 incorrectly yielded an assert in LLVM IR lowering #2584.
  • Fix issue when tests encounter a segmentation fault or similar.
  • With project.json, when overriding with an empty list the base settings would still be used. #2583
  • Add sigsegv stacktrace in test and regular errors for Darwin Arm64. #1105
  • Incorrect error message when using generic type that isn't imported #2589
  • String.to_integer does not correctly return in some cases where it should #2590.
  • Resolving a missing property on a const enum with inline, reached an assert #2597.
  • Unexpected maybe-deref subscript error with out parameter #2600.
  • Bug on rethrow in return with defer #2603.
  • Fix bug when converting from vector to distinct type of wider vector. #2604
  • $defined(hashmap.init(mem)) causes compiler segfault #2611.
  • Reference macro parameters syntax does not error in certain cases. #2612
  • @param name parsing too lenient #2614.

Stdlib changes

  • Add CGFloat CGPoint CGSize CGRect types to core_foundation (macOS).
  • Add NSStatusItem const enum to ns module (macOS).
  • Add NSWindowCollectionBehavior NSWindowLevel NSWindowTabbingMode to ns module (macOS).
  • Add ns::eventmask_from_type function to objc (macOS).
  • Deprecate objc enums in favour of const inline enums backed by NS numerical types, and with the NS prefix, to better align with the objc api (macOS).
  • Deprecate event_type_from function in favour of using NSEvent directly, to better align with the objc api (macOS).
  • Add unit tests for objc and core_foundation (macOS).
  • Make printing typeids give some helpful typeid data.
  • Add NSApplicationTerminateReply to ns module (macOS).
  • Add registerClassPair function to objc module (macOS).
  • Somewhat faster BigInt output.
  • Cache printf output.

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.

Discuss this article on Reddit and Hacker News.

C3 Language at 0.7.7: Vector ABI, RISCV improvements and more

0.7.7 is a major advance in C3 usability with vector ABI changes. It also contains several small quality-of-life additions, such as the ability to splat structs into an initializer, and implicit subscript dereferencing. Fairly few bugs were discovered during this development cycle, which is why the fixed bugs are unusually low.

Let's look at what 0.7.7 brings in more detail:

Vector ABI changes

The most significant change in this release is the ABI change for vectors, which now store and pass vectors as arrays in function calls and structs. While vectors still use SIMD, their equality to arrays on the ABI level means that C graphical libraries will directly match vector types.

Where before you needed to work with C structs defining vectors and then converting them to SIMD vectors for actual computation, it now works out of the box. Another problem with vectors prior to 0.7.7 was their space and alignment requirements over structs. From 0.7.7 alignment matches that of structs and arrays, making them extremely convenient to work with.

For cases where SIMD vectors are actually expected, it's possible to create distinct types using typedef with a new @simd attribute to exactly match standard C SIMD vectors, e.g. typedef V4si = int[<4>] @simd;. This then exactly matches the corresponding C SIMD type.

This makes it easier than ever to use SIMD with C3.

An example:

// Pre 0.7.7
union Vec3
{
    struct
    {
        float x, y, z;
    }
    float[3] arr;
}
extern fn void draw_image(Image* image, Vec3 pos);

fn void update()
{
    ...
    // Speed and position is stored as Vec3
    float[<3>] speed = ball.speed.arr; // Implicit conversion array to vector 
    float[<3>] position = ball.position.arr; // Implicit conversion array to vector
    ball.position = (position + speed);
}
// 0.7.7+
alias Vec3 = float[<3>]; // Equivalent to the struct due to ABI change
extern fn void draw_image(Image* image, Vec3 pos);

fn void update()
{
    ...
    // Speed and position is stored as Vec3
    ball.position += ball.speed; // SIMD add
}

Struct initializer splats

This feature enables using the splat operator ... to give a designated initializer default values that are overridden by the following arguments.

struct Foo
{
    int a;
    double b;
    String c;
}

fn void test()
{
    Foo f = { 1, 2.3, "Hi" };
    Foo f2 = { ...f, .a = 8, .c = "Bye" }; // Results in { 8, 2.3, "Bye" }
}

Subscript deref

When passing arrays or lists by reference, the [] operator tend to behave in an undesirable way, dereferencing the pointer instead of the underlying array/list:

fn void test(List{int}* list_ref, int[3]* array_ref, int[3] array)
{
    // WRONG, would yield a 'List{int}' not an int
    // int val = list_ref[1]; 
    int val = (*list_ref)[1]; // Correct
    int val2 = list_ref.get(1); // Also correct, uses implicit deref of '.'
    // Wrong, would yield an 'int[3]', not an int
    // int val3 = array_ref[1]; 
    int val3 = (*array_ref)[1]; // Correct
    int val4 = array[1];
}

Subscript deref addresses this. Using .[1] will dereference if needed:

fn void test(List{int}* list_ref, int[3]* array_ref, int[3] array)
{
    int val = list_ref.[1]; 
    int val2 = array_ref.[1];
    int val3 = array.[1]; // Works even though it isn't a pointer.
}

This is helpful when writing macros and such that will want to accept both elements by reference and by value:

macro third_element(x)
{
    return x.[2];
}
fn void test()
{
    int[3] arr;
    int[] slice = &arr;
    third_element(arr);   // Works
    third_element(slice); // Works
    third_element(&arr);  // Also works thanks to subscript deref
}

Typedef with alignment

A new feature for typedef is to allow creating a type with a specific alignment without wrapping it in a struct. We may, for example, create an integer that is 16 bit aligned using typedef Int2 = int @align(2);. This is an alternative way to safely work with references to under-aligned members in packed structs.

// Pre 0.7.7
struct Foo @packed
{
    char a;
    int b;
}
fn void test()
{
    Foo f = { 'a', 1 };
    int* b_ref = &f.b;
    @unaligned_store(*b_ref, 2, 1); // Valid
    *b_ref = 2; // Error at runtime in safe mode, unaligned access
}
// 0.7.7+
typedef IntAlign1 = int @align(1);
struct Foo @packed
{
    char a;
    IntAlign1 b;
}
fn void test()
{
    Foo f = { 'a', 1 };
    IntAlign1* b_ref = &f.b;
    *b_ref = 2;
}

More string functions at compile time

@str_snakecase, @str_constantcase, @str_pascalcase and @str_replace macros are added to modify strings at compile time efficiently for certain macro manipulation at compile time.

fn void test()
{
    String $test = "HelloWorld";
    $echo @str_snakecase($test);               // echoes "hello_world"
    $echo @str_constantcase($test);            // echoes "HELLO_WORLD"
    String $test2 = "hello_world";
    $echo @str_pascalcase($test2);             // echoes "HelloWorld"
    $echo @str_replace($test, "Hello", "Bye"); // echoes "ByeWorld"
}

Small but important changes

Aliases that refer to @local variables must themselves have local visibility. @extern is renamed @cname as it was frequently misunderstood. Generic inference now works better in initializers. For slices with the .. syntax, it's now possible to have the end index be one less than the starting index, so that zero size slices can be expressed with the .. syntax as well.

Cross-Platform and Architecture Support Expansion

This release significantly strengthens C3C's cross-platform capabilities, particularly for RISC-V architecture support. It's now possible to set individual CPU features using --cpu-flags, e.g. --cpu-flags +avx,-sse. For RISC-V, --riscv-cpu has been added, as well as renaming the RISC-V abi flag to the more correct --riscv-abi.

Stdlib changes

The sorting macros accidentally only took non-slices by value, which would work in some cases but not in others. This has been fixed, but might mean that some code needs to update as well. TcpSocketPair was added to the tcp module to create a bidirectional local socket pair, and using sockets on Windows should now implicitly initialize the underlying socket subsystem.

Fixes

0.7.7 has only about 11 fixes, which reflects the relatively few bugs encountered in the 0.7.7 cycle. There are outstanding bugs on the inline asm, which has a significant update planned. The most important fix is patching a regression for MacOS which prevented backtrace printing.

Looking Forward

With the updated Vector ABI and the change from @extern to @cname there are a lot of vendor libraries that will need a refresh. There is also a new matrix library in development that hopefully might get included in the next release. There is more functionality to add for fine-tuning processor capabilities for both RISC-V, but also AArch64. There have also been requests for 32-bit Arm support, but the lack of CI tests for different Arm processors is blocking it at the moment.

Community and Contributions

This release wouldn't have been possible without the C3 community. I'd like to extend a deep thank you to all who have contributed, both through filed issues, PRs and just plain discussions.

Change Log

Click for full change log

Changes / improvements

  • Error when using $vaarg/$vacount/$vasplat and similar in a macro without vaargs #2510.
  • Add splat defaults for designated initialization #2441.
  • Add new builtins $$str_snakecase $$str_replace and $$str_pascalcase.
  • "build-dir" option now available for project.json, added to project. #2323
  • Allow .. ranges to use "a..a-1" in order to express zero length.
  • Disallow aliasing of @local symbols with a higher visibility in the alias.
  • Add --max-macro-iterations to set macro iteration limit.
  • Improved generic inference in initializers #2541.
  • "Maybe-deref" subscripting foo.[i] += 1 #2540.
  • ABI change for vectors: store and pass them as arrays #2542.
  • Add @simd and @align attributes to typedef #2543.
  • Rename @extern to @cname, deprecating the old name #2493.
  • Allow (Foo)0 bitstruct casts even if type sizes do not match.
  • The option --riscvfloat renamed --riscv-abi.
  • Add initial --cpu-flags allowing fine grained control over CPU features.
  • Add --riscv-cpu settings for RISC-V processors #2549.

Fixes

  • Bug in io::write_using_write_byte.
  • Bitstruct value cannot be used to index a const array in compile time. #2512
  • Compiler fails to stop error print in recursive macro, and also prints unnecessary "inline at" #2513.
  • Bitstruct truncated constant error escapes $defined #2515.
  • Compiler segfault when accessing member of number cast to bitstruct #2516.
  • Compiler assert when getting a member of a bitstruct : char @bigendian #2517.
  • Add ??? and +++= to list-precedence.
  • Fix issues with linking when using symbol aliases. #2519
  • Splatting optional compile-time macro parameter from inside lambda expression does not work #2532.
  • Compiler segfault when getting a nonexistant member from an unnamed struct #2533.
  • Correctly mention aliased type when method is not implemented #2534.
  • Regression: Not printing backtrace when tests fail for MacOS #2536.

Stdlib changes

  • Sorting functions correctly took slices by value, but also other types by value. Now, only slices are accepted by value, other containers are always by ref.
  • Added @str_snakecase, @str_replace and @str_pascalcase builtin compile time macros based on the $$ builtins.
  • Add TcpSocketPair to create a bidirectional local socket pair.
  • Add extern fn CInt socketpair(AIFamily domain, AISockType type, CInt protocol, NativeSocket[2]* sv) binding to posix.
  • Add extern fn getsockname(NativeSocket socket, SockAddrPtr address, Socklen_t* address_len) binding to win32.

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.

Discuss this article on Reddit.

C3 Language at 0.7.6: Shebang, generic inference and lengthof()

The C3 programming language continues its steady evolution with version 0.7.6, focusing on quality-of-life improvements and language refinements While previous 0.7.x versions have seen some notable additions to the language itself, 0.7.6 only adds a few minor features, with nothing new planned for 0.7.7. Originally the inline asm updates and fixes were scheduled for 0.7.6, the discussion around the semantics took too much time for it to make it to 0.7.6.

In other news, C3 is now tracked by Linguist on GitHub which means code on GitHub will finally have highlighting for C3 files.

New Features and Improvements

  • Built-in functionlengthof() - The new lengthof function provides a unified way to use the result of the method or function tagged @operator(len). For builtin types with length, such as arrays, this will lower into a .len field access. This allows building macros that leverage @operator(len) without having to use foreach.
  • Shebang Support: C3 source files now support #! comments on the first line, simplifying using C3 code in scripts.
  • Generic parameter inference: - Generic parameter inference from left to right is added: List{int} x = list::NOHEAP;.
  • A new +++= operator - This complements the +++ compile time concatenation operator to make code that use repeated concatenation shorter and more concise.
  • Enhanced $defined - $defined now also supports $nameof, $offsetof and $alignof, so that the code can query if the builtins are supported for the particular argument. This is particularly useful for lazy arguments.
  • Slice and Array Comparisons: User-defined types implementing the == operator overload can now be properly compared when used in slices and arrays.
  • Project version builtin: You can now access your project version using env::PROJECT_VERSION. It reflects exactly the string in project.json.

Standard Library Expansions

  • InterfaceList: - A new container for storing values that implement specific interfaces, enabling more flexible polymorphic programming.
  • Enhanced LinkedList: - Now supports array view operations including [] indexing and both forward and reverse foreach iteration (#2438).
  • Cross-Platform File System Support: - Directory location support has been added for home, documents, downloads and other folders in the path module.
  • Added I/O Operations: - io::skip for skipping data and little-endian family for read/write operations io::read_le, io::write_le.
  • CVaList support: - Added MacOS AArch64 and Linux/MacOS x64 valist support with CVaList.

Community and Contributions

This release wouldn't have been possible without the incredible C3C community. The collaborative effort in identifying, reporting, and fixing the numerous issues addressed in this release exemplifies the strength of the C3C development community.

The C3 team remains committed to creating a modern, safe, and fast programming language that doesn't compromise on the low-level control that makes C so enduring. Thank you to everyone who has contributed to making this release possible!

Change Log

Click for full change log

Changes / improvements

  • Add lengthof() compile time function #2439
  • Allow doc comments on individual struct members, faultdefs and enum values #2427.
  • $alignof, $offsetof and $nameof can now be used in $defined.
  • Infer generic parameters lhs -> rhs: List{int} x = list::NOHEAP.
  • Unify generic and regular module namespace.
  • env::PROJECT_VERSION now returns the version in project.json.
  • Comparing slices and arrays of user-defined types that implement == operator now works #2486.
  • Add 'loop-vectorize', 'slp-vectorize', 'unroll-loops' and 'merge-functions' optimization flags #2491.
  • Add exec timings to -vv output #2490.
  • Support #! as a comment on the first line only.
  • Add +++= operator.

Fixes

  • Compiler assert with var x @noinit = 0 #2452
  • Confusing error message when type has [] overloaded but not []= #2453
  • $defined(x[0] = val) causes an error instead of returning false when a type does not have []= defined #2454
  • Returning pointer to index of slice stored in a struct from method taking self incorrectly detected as returning pointer to local variable #2455.
  • Inlining location when accessing #foo symbols.
  • Improve inlined-at when checking generic code.
  • Fix codegen bug in expressions like foo(x()) ?? io::EOF? causing irregular crashes.
  • Correctly silence "unsupported architecture" warning with --quiet #2465
  • Overloading &[] should be enough for foreach. #2466
  • Any register allowed in X86_64 inline asm address. #2463
  • int val = some_int + some_distinct_inline_int errors that int cannot be cast to DistinctInt #2468
  • Compiler hang with unaligned load-store pair. #2470
  • ?? with void results on both sides cause a compiler crash #2472.
  • Stack object size limit error on a static object. #2476
  • Compiler segfault when modifying variable using an inline assembly block inside defer #2450.
  • Compile time switch over type would not correctly compare function pointer types.
  • Regression: Compiler segfault when assigning struct literal with too few members #2483
  • Fix compile time format check when the formatting string is a constant slice.
  • Compiler segfault for invalid e-mails in project.json. #2488
  • Taking .ordinal from an enum passed by pointer and then taking the address of this result would return the enum, not int.
  • Alias and distinct types didn't check the underlying type wasn't compile time or optional.
  • Incorrect nameof on nested struct names. #2492
  • Issue not correctly aborting compilation on recursive generics.
  • Crash during codegen when taking the typeid of an empty enum with associated values.
  • Assert when the binary doesn't get created and --run-once is used. #2502
  • Prevent foo.bar = {} when bar is a flexible array member. #2497
  • Fix several issues relating to multi-level inference like int[*][*] #2505
  • $for int $a = 1; $a < 2; $a++ would not parse.
  • Fix lambda-in-macro visibility, where lambdas would sometimes not correctly link if used through a macro.
  • Dead code analysis with labelled if did not work properly.
  • Compiler segfault when splatting variable that does not exist in untyped vaarg macro #2509

Stdlib changes

  • Added Advanced Encryption Standard (AES) algorithm (ECB, CTR, CBC modes) in std::crypto::aes.
  • Added generic InterfaceList to store a list of values that implement a specific interface
  • Added path::home_directory, path::documents_directory, path::videos_directory, path::pictures_directory, path::desktop_directory, path::screenshots_directory, path::public_share_directory, path::templates_directory, path::saved_games_directory, path::music_directory, path::downloads_directory.
  • Add LinkedList array_view to support [] and foreach/foreach_r. #2438
  • Make LinkedList printable and add == operator. #2438
  • CVaList support on MacOS aarch64, SysV ABI x64.
  • Add io::skip and io::read_le and io::write_le family of functions.

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.

Discuss this article on Reddit or Hacker News.

C3 Language at 0.7.5: Language tweaks and conveniences

The C3 programming language has reached 0.7.5, marking another milestone in the language's evolution. This release brings improvements to language features, developer experience, and standard library functionality.

Here's what's new and improved in this update.

Major Language Features

Module Aliasing Support

One of the new features in 0.7.5 is the introduction of module aliasing with the syntax alias foo = module std::io. This enhancement improves user control over code organization and readability, allowing developers to create shorter names for modules where needed.

Enhanced Compile-Time Capabilities

The compile-time system has received several additions: - Optional macro params: Gives macros arguments that are optional without requiring a default value. - ???: Compile time ternary, guaranteed to resolve at compile time and will not execute the false branch. - @safeinfer: Enables the use of var in function contexts, where it was previously disallowed. - New mathematical builtins: @intlog2, @clz, @min, @max and functions are now available at compile time. - bitsizeof macro builtin: Provides bit-level size information for types.

Operator Overloading Evolution

C3 0.7.5 brings improvements to operator overloading: - @operator(==) now also enables switch statement support for the type. - Enhanced chained array access: foo[x][y] = b now can pass through multiple levels of overloads, and works as expected with proper overload resolution. - Type.is_eq now correctly returns true for types with equality overloads.

Type System Enhancements
  • $kindof: Shorthand for $typeof(...).kindof which simplifies contract checks.
  • Implicit type conversions: Types now convert to typeid implicitly, streamlining contracts and compile time programming.
  • Enhanced $defined: It now accepts declarations, like $defined(int x = y) which removes the need for macros like @assignable_to.
  • Struct inheritance: Struct and typedef subtypes now inherit dynamic methods.

Developer Experience Improvements

Better Error Messages and Safety

The compiler now provides more helpful diagnostics: - Improved error messages for missing $endif and missing if bodies. - Better directory creation error messages in project and library creation - Huge stack object overflow protection with configurable --max-stack-object-size

Build System Enhancements
  • Library management: c3l-libraries now package linked libraries in a directory specified by the "linklib-dir" setting.
  • Cross-platform improvements: Enhanced support for different operating systems and architectures

Standard Library Expansion

New Data Structures and Utilities

The standard library has grown: - FileMmap: Memory-mapped file management - FixedBlockPool: Memory pool for fixed-size objects - HashSet: Generic hash set implementation with values method support - AsciiCharset: Fast ASCII character matching utilities - Logging system: Introduction of std::core::log for common logging .

Enhanced String Operations

String manipulation gets a boost with: - String.contains_char: Character containment checking - String.trim_charset: Trimming based on character sets - Functional array operations: New macros including @zip, @reduce, @filter, @any, @all, @sum, @product and @indices_of.

Breaking Changes and Deprecations

Important Deprecations

Several features have been deprecated in favor of improved alternatives: - @compact comparison behavior (use --use-old-compact-eq for compatibility) - add_array in favor of push_all on lists. - @assignable_to in favor of using $define. - @typekind in favor of using $kindof. - @typeis in favor of $typeof(foo) == Type. - @select in favor of $foo ??? #expr1 : #expr2.

Performance and Bug Fixes

Critical Fixes

This release addresses numerous important issues: - ASAN triggering fixes in List.remove_at - AVX512 vector handling corrections - Codegen improvements for if-try expressions - Memory allocation optimizations for 32-bit machines - Recursive generic creation detection

Platform-Specific Improvements
  • Enhanced Android and OpenBSD support
  • Improved native CPU detection
  • Better cross-compilation support

Looking Forward

Deprecations of many type introspection macros, such as @typekind and @typeis is together with the improvements in $defined, the implicit type conversions to typeid and $kindof spearheading a shift to making constraint checking succinct while also being completely obvious. Relying on macros would often make the constraints less clear to a reader. On top of this we get compile time ternary using ??? : to succinctly express compile time selection between two expressions. With the changes, the code is as short to type but without the need to remember particulars of one macro over the other.

Overall, C3 0.7.5 represents another step in maturing the language's core features while laying the groundwork for future enhancements. The focus will continue to be aimed at improve developer experience, performance, and language consistency. Many of the standard library additions are contributions from the community around C3, which is providing essential feedback and direction to polish the language further. C3 is step-by-step establishing itself as a modern evolution of C that maintains simplicity while adding powerful abstractions.

Demo

For a deeper look at the changes, watch the demo.

Change Log

Click for full change log

Changes / improvements

  • Support alias foo = module std::io module aliasing.
  • Add compile-time @intlog2 macro to math.
  • Add compile-time @clz builtin. #2367
  • Add bitsizeof macro builtins. #2376
  • Add compile-time @min and @max builtins. #2378
  • Deprecate @compact use for comparison. Old behaviour is enabled using --use-old-compact-eq.
  • Switch available for types implementing @operator(==).
  • Type.is_eq is now true for types with == overload.
  • Methods ignore visibility settings.
  • Allow inout etc on untyped macro parameters even if they are not pointers.
  • Deprecate add_array in favour of push_all on lists.
  • Fix max module name to 31 chars and the entire module path to 63 characters.
  • Improve error message for missing $endif.
  • foo[x][y] = b now interpreted as (*&foo[x])[y] = b which allows overloads to do chained [] accesses.
  • Error if a stack allocated variable is too big (configurable with --max-stack-object-size).
  • Add @safeinfer to allow var to be used locally.
  • Types converts to typeid implicitly.
  • Allow $defined take declarations: $defined(int x = y)
  • Struct and typedef subtypes inherit dynamic functions.
  • Improved directory creation error messages in project and library creation commands.
  • @assignable_to is deprecated in favour of $define
  • Add linklib-dir to c3l-libraries to place their linked libraries in. Defaults to linked-libs
  • If the os-arch linked library doesn't exist, try with os for c3l libs.
  • A file with an inferred module may not contain additional other modules.
  • Update error message for missing body after if/for/etc #2289.
  • @is_const is deprecated in favour of directly using $defined.
  • @is_lvalue(#value) is deprecated in favour of directly using $defined.
  • Added $kindof compile time function.
  • Deprecated @typekind macro in favour of $kindof.
  • Deprecated @typeis macro in favour of $typeof(#foo) == int.
  • $defined(#hash) will not check the internal expression, just that #hash exists. Use $defined((void)#hash) for the old behaviour.
  • Added optional macro arguments using macro foo(int x = ...) which can be checked using $defined(x).
  • Add compile time ternary $val ??? <expr> : <expr>.

Fixes

  • List.remove_at would incorrectly trigger ASAN.
  • With avx512, passing a 512 bit vector in a union would be lowered incorrectly, causing an assert. #2362
  • Codegen error in if (try x = (true ? io::EOF? : 1)), i.e. using if-try with a known Empty.
  • Codegen error in if (try x = (false ? io::EOF? : 1)), i.e. using if-try with a CT known value.
  • Reduce allocated Vmem for the compiler on 32 bit machines.
  • Bug causing a compiler error when parsing a broken lambda inside of an expression.
  • Fixed: regression in comments for @deprecated and @pure.
  • Detect recursive creation of generics #2366.
  • Compiler assertion when defining a function with return type untyped_list #2368.
  • Compiler assert when using generic parameters list without any parameters. #2369
  • Parsing difference between "0x00." and "0X00." literals #2371
  • Fixed bug generating $c += 1 when $c was derived from a pointer but behind a cast.
  • Compiler segfault when using bitwise not on number literal cast to bitstruct #2373.
  • Formatter did not properly handle "null" for any, and null for empty faults. #2375
  • Bitstructs no longer overloadable with bitops. #2374
  • types::has_equals fails with assert for bitstructs #2377
  • Fix native_cpus functionality for OpenBSD systems. #2387
  • Assert triggered when trying to slice a struct.
  • Improve codegen for stack allocated large non-zero arrays.
  • Implement a5hash in the compiler for compile-time $$str_hash to match String.hash().
  • Functions being tested for overload are now always checked before test.
  • Compile time indexing at compile time in a $typeof was no considered compile time.
  • Slicing a constant array with designated initialization would not update the indexes.
  • Fix for bug when @format encountered * in some cases.
  • Compiler segfault on global slice initialization with null[:0] #2404.
  • Use correct allocator in replace.
  • Regression: 1 character module names would create an error.
  • Compiler segfault with struct containing list of structs with an inline member #2416
  • Occasionally when using macro method extensions on built-in types, the liveness checker would try to process them. #2398
  • Miscompilation of do-while when the while starts with a branch #2394.
  • Compiler assert when calling unassigned CT functions #2418.
  • Fixed crash in header generation when exporting functions with const enums (#2384).
  • Fix incorrect panic message when slicing with negative size.
  • Incorrect type checking when &[] and [] return optional values.
  • Failed to find subscript overloading on optional values.
  • Socket.get_option didn't properly call getsockopt, and getsockopt had an invalid signature.
  • Taking the address of a label would cause a crash. #2430
  • @tag was not allowed to repeat.
  • Lambdas on the top level were not exported by default. #2428
  • has_tagof on tagged lambdas returns false #2432
  • Properly add "inlined at" for generic instantiation errors #2382.
  • Inlining a const as an lvalue would take the wrong path and corrupt the expression node.
  • Grabbing (missing) methods on function pointers would cause crash #2434.
  • Fix alignment on jump table.
  • Fix correct ? after optional function name when reporting type errors.
  • Make log and exp no-strip.
  • @test/@benchmark on module would attach to interface and regular methods.
  • Deprecated @select in favor of ???.
  • Enum inference, like Foo x = $eval("A"), now works correctly for $eval.
  • Fix regression where files were added more than once. #2442
  • Disambiguate types when they have the same name and need cast between each other.
  • Compiler module-scope pointer to slice with offset, causes assert. #2446
  • Compiler hangs on == overload if other is generic #2443
  • Fix missing end of line when encountering errors in project creation.
  • Const enum methods are not being recognized. #2445
  • $defined returns an error when assigning a struct initializer with an incorrect type #2449

Stdlib changes

  • Add == to Pair, Triple and TzDateTime. Add print to Pair and Triple.
  • Add OpenBSD to env::INET_DEVICES and add required socket constants.
  • Added FileMmap to manage memory mapped files.
  • Add vm::mmap_file to memory map a file.
  • Updated hash functions in default hash methods.
  • Added FixedBlockPool which is a memory pool for fixed size blocks.
  • Added the experimental std::core::log for logging.
  • Added array @zip and @zip_into macros. #2370
  • Updated termios bindings to use bitstructs and fixed some constants with incorrect values #2372
  • Add Freestanding OS types to runtime env:: booleans.
  • Added libloaderapi to std::os::win32.
  • Added HashSet.values and String.contains_char #2386
  • Added &[] overload to HashMap.
  • Deprecated PollSubscribes and PollEvents in favour of PollSubscribe and PollEvent and made them const enums.
  • Added AsciiCharset for matching ascii characters quickly.
  • Added String.trim_charset.
  • Added array @reduce, @filter, @any, @all, @sum, @product, and @indices_of macros.
  • String.bformat has reduced overhead.
  • Supplemental roundeven has a normal implementation.

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.