2026-01-29
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.
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.
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 bazbaz({double}(1.3, 0.5); // Instantiates baz onlyAdding 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}();}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"}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}}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.
Previous to 0.7.9 the following held:
int? y = 1; // Asign a regular valueint? x = io::EOF?; // Assign the excuse io::EOF to, making it an Empty optionalint z1 = x!; // Rethrow if x is Emptyint z2 = x!!; // Panic if x is Emptyint z3 = x ?? 3; // Return 3 if x is EmptyUnfortunately 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 itio::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.
--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.
Improvements have been made for NetBSD support as a target.
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;}$$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.
On win32 utf-8 console output is now enabled by default in compiled programs.
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.
Thanks to the hard work of contributors exploring corner-cases plus the uncommonly long development time, this version contains over 120 fixes(!).
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.
This version adds: Poly1305, Ripemd, Chacha20, Blake2, Blake3 and streebog.
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.
@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.
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.
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.
--custom-libc option for custom libc implementations.$$mask_to_int and $$int_to_mask to create bool masks from integers and back.<...> rather than module based.$$VERSION and $$PRERELEASE compile time constants.~ instead of ?. return io::EOF?; becomes return io::EOF~.? to create optional.foo.$abc implicitly mean foo.eval("$abc").int[*][*] is deprecated and will be removed 0.8.0.{ .b, .c = 123 }.Bytebuffer.grow was broken #2622."\x80" would be incorrectly lowered. #2623$defined and $sizeof #2633.char[10] x = "abcd.$defined(foo()) now correctly errors if foo() would require a path.@if($defined((char*){}.foo())) does not error if foo is missing.$$LINE would sometimes yield the incorrect format.tmem without @pool would cause errors in unrelated tests. #2654i<n> suffixes were not caught when n < 8, causing an assert.$defined was not handled correctly, leading to an assertion.foo.x was not always handled correctly when foo was optional.x'1234' +++ (ichar[1]) { 'A' } would fail due to missing const folding.String.tokenize_all would yield one too many empty tokens at the end.String.replace no longer depends on String.split.\u<unicode char> could crash the compiler on some platforms.io::read_fully now handles unbounded streams properly.LinkedList.to_format to properly iterate linked list for printing.temp_directory on Windows #2762.$defined with function call missing arguments would cause a crash. #2771@if processing was not detected, causing a crash instead of an error. #2771int[*][6] was given too few values, the compiler would assert instead of giving an error. #2771int? ? was not correctly handled. #2786$Type* where $Type is an optional type #2848io::EOF~! in various unhandled places. #2848ThreadPool join function to wait for all threads to finish in the pool without destroying the threads.@in compile-time macro to check for a value in a variable list of constants. #2662destroy() is now “@maydiscard” and should be ignored. It will return void in 0.8.0.unlock() and lock() is now “@maydiscard” and should be ignored. They will return void in 0.8.0.signal() broadcast() and wait() are now “@maydiscard”. They will return void in 0.8.0.detatch() is now “@maydiscard”. It will return void in 0.8.0.@maydiscard on a set of functions. They will return void in 0.8.0.join() is now “@maydiscard”.poly1305 one-time Message Authentication Code and associated tests. #2639ripemd hashing and associated tests. #2663chacha20 stream cipher and associated tests. #2643BLAKE2 (optionally keyed) cryptographic hashing with associated tests. #2648BLAKE3 XOF and associated tests. #2667Elf32_Shdr and Elf64_Shdr to std::os::linux.any.to and any.as.DString.append_chars, use DString.append_stringDString.append_string for DStrings, use DString.append_dstring instead.DString.append_bytes.streebog (aka “GOST-12”) hashing with 256-bit and 512-bit outputs. #2659AsciiCharset constants and combine its related compile-time/runtime macros. #2688Printable struct for ansi RGB formatting instead of explicit allocation and deprecate the old method.mem::store and mem::load which may combine both aligned and volatile operations.EMPTY_MACRO_SLOT and its related uses, in favor of optional_param = ... named macro arguments. #2805realloc_array, realloc_array_aligned, and realloc_array_try to allocator::. #2760Check 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.