Skip to content

2026

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.