Skip to content

2026

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.