2025-10-30
0.7.7 is a major advance in C3 usability with vector ABI changes. It also contains several small quality-of-life additions, such as the ability to splat structs into an initializer, and implicit subscript dereferencing. Fairly few bugs were discovered during this development cycle, which is why the fixed bugs are unusually low.
Let’s look at what 0.7.7 brings in more detail:
The most significant change in this release is the ABI change for vectors, which now store and pass vectors as arrays in function calls and structs. While vectors still use SIMD, their equality to arrays on the ABI level means that C graphical libraries will directly match vector types.
Where before you needed to work with C structs defining vectors and then converting them to SIMD vectors for actual computation, it now works out of the box. Another problem with vectors prior to 0.7.7 was their space and alignment requirements over structs. From 0.7.7 alignment matches that of structs and arrays, making them extremely convenient to work with.
For cases where SIMD vectors are actually expected, it’s possible to create distinct types using typedef with a new @simd attribute to exactly match standard C SIMD vectors, e.g. typedef V4si = int[<4>] @simd;. This then exactly matches the corresponding C SIMD type.
This makes it easier than ever to use SIMD with C3.
An example:
// Pre 0.7.7union Vec3{ struct { float x, y, z; } float[3] arr;}extern fn void draw_image(Image* image, Vec3 pos);
fn void update(){ ... // Speed and position is stored as Vec3 float[<3>] speed = ball.speed.arr; // Implicit conversion array to vector float[<3>] position = ball.position.arr; // Implicit conversion array to vector ball.position = (position + speed);}// 0.7.7+alias Vec3 = float[<3>]; // Equivalent to the struct due to ABI changeextern fn void draw_image(Image* image, Vec3 pos);
fn void update(){ ... // Speed and position is stored as Vec3 ball.position += ball.speed; // SIMD add}This feature enables using the splat operator ... to give a designated initializer default values that are overridden by the following arguments.
struct Foo{ int a; double b; String c;}
fn void test(){ Foo f = { 1, 2.3, "Hi" }; Foo f2 = { ...f, .a = 8, .c = "Bye" }; // Results in { 8, 2.3, "Bye" }}When passing arrays or lists by reference, the [] operator tend to behave in an undesirable way, dereferencing the pointer instead of the underlying array/list:
fn void test(List{int}* list_ref, int[3]* array_ref, int[3] array){ // WRONG, would yield a 'List{int}' not an int // int val = list_ref[1]; int val = (*list_ref)[1]; // Correct int val2 = list_ref.get(1); // Also correct, uses implicit deref of '.' // Wrong, would yield an 'int[3]', not an int // int val3 = array_ref[1]; int val3 = (*array_ref)[1]; // Correct int val4 = array[1];}Subscript deref addresses this. Using .[1] will dereference if needed:
fn void test(List{int}* list_ref, int[3]* array_ref, int[3] array){ int val = list_ref.[1]; int val2 = array_ref.[1]; int val3 = array.[1]; // Works even though it isn't a pointer.}This is helpful when writing macros and such that will want to accept both elements by reference and by value:
macro third_element(x){ return x.[2];}fn void test(){ int[3] arr; int[] slice = &arr; third_element(arr); // Works third_element(slice); // Works third_element(&arr); // Also works thanks to subscript deref}A new feature for typedef is to allow creating a type with a specific alignment without wrapping it in a struct. We may, for example, create an integer that is 16 bit aligned using typedef Int2 = int @align(2);. This is an alternative way to safely work with references to under-aligned members in packed structs.
// Pre 0.7.7struct Foo @packed{ char a; int b;}fn void test(){ Foo f = { 'a', 1 }; int* b_ref = &f.b; @unaligned_store(*b_ref, 2, 1); // Valid *b_ref = 2; // Error at runtime in safe mode, unaligned access}// 0.7.7+typedef IntAlign1 = int @align(1);struct Foo @packed{ char a; IntAlign1 b;}fn void test(){ Foo f = { 'a', 1 }; IntAlign1* b_ref = &f.b; *b_ref = 2;}@str_snakecase, @str_constantcase, @str_pascalcase and @str_replace macros are added to modify strings at compile time efficiently for certain macro manipulation at compile time.
fn void test(){ String $test = "HelloWorld"; $echo @str_snakecase($test); // echoes "hello_world" $echo @str_constantcase($test); // echoes "HELLO_WORLD" String $test2 = "hello_world"; $echo @str_pascalcase($test2); // echoes "HelloWorld" $echo @str_replace($test, "Hello", "Bye"); // echoes "ByeWorld"}Aliases that refer to @local variables must themselves have local visibility. @extern is renamed @cname as it was frequently misunderstood. Generic inference now works better in initializers. For slices with the .. syntax, it’s now possible to have the end index be one less than the starting index, so that zero size slices can be expressed with the .. syntax as well.
This release significantly strengthens C3C’s cross-platform capabilities, particularly for RISC-V architecture support. It’s now possible to set individual CPU features using --cpu-flags, e.g. --cpu-flags +avx,-sse. For RISC-V, --riscv-cpu has been added, as well as renaming the RISC-V abi flag to the more correct --riscv-abi.
The sorting macros accidentally only took non-slices by value, which would work in some cases but not in others. This has been fixed, but might mean that some code needs to update as well. TcpSocketPair was added to the tcp module to create a bidirectional local socket pair, and using sockets on Windows should now implicitly initialize the underlying socket subsystem.
0.7.7 has only about 11 fixes, which reflects the relatively few bugs encountered in the 0.7.7 cycle. There are outstanding bugs on the inline asm, which has a significant update planned. The most important fix is patching a regression for MacOS which prevented backtrace printing.
With the updated Vector ABI and the change from @extern to @cname there are a lot of vendor libraries that will need a refresh. There is also a new matrix library in development that hopefully might get included in the next release. There is more functionality to add for fine-tuning processor capabilities for both RISC-V, but also AArch64. There have also been requests for 32-bit Arm support, but the lack of CI tests for different Arm processors is blocking it at the moment.
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.
$$str_snakecase $$str_replace and $$str_pascalcase."build-dir" option now available for project.json, added to project. #2323.. ranges to use “a..a-1” in order to express zero length.@local symbols with a higher visibility in the alias.--max-macro-iterations to set macro iteration limit.foo.[i] += 1 #2540.@extern to @cname, deprecating the old name #2493.(Foo)0 bitstruct casts even if type sizes do not match.--riscvfloat renamed --riscv-abi.--cpu-flags allowing fine grained control over CPU features.--riscv-cpu settings for RISC-V processors #2549.io::write_using_write_byte.$defined #2515.bitstruct : char @bigendian #2517.@str_snakecase, @str_replace and @str_pascalcase builtin compile time macros based on the $$ builtins.extern fn CInt socketpair(AIFamily domain, AISockType type, CInt protocol, NativeSocket[2]* sv) binding to posix.extern fn getsockname(NativeSocket socket, SockAddrPtr address, Socklen_t* address_len) binding to win32.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.