2025-03-30
Originally I was going to write this as normal release notes, but it’s a bit too much to go through the whys and wherefores of the changes. So the format of this blog post is going to be a bit different.
0.6.0 was tagged in June 2024, and since then there’s been monthly releases up until the last one, 0.6.8. While the language has evolved during this year, each 0.6.x version has been backwards compatible with code written for previous 0.6 versions. Despite this constraint, there have been a lot of improvements, especially in enhancing compile time to have much fewer corners, and of course the standard library has been improved as well.
Well worth mentioning is the impact Tsoding had on the visibility of the language. His “recreational programming” streams on C3 brought a lot of new users to the community, and with it a lot of extremely valuable feedback on both the langage and the standard library.
The release format of C3 allows for breaking changes in every new 0.x release. This means that 0.7.0 and 0.6.8 code will not be compatible. Some of this is already deprecated code in 0.6.8, but for some things there were no clear migration path.
For that reason, I’d like to start by giving an primer on how to convert 0.6.x code to 0.7.0
While these changes may seem like a lot, most of the changes were actually gradually introduced in 0.6.x with the now removed variants are being deprecated.
{} rather than (<>). E.g. List(<int>) x; -> List {int} x;def has been replaced by alias, syntax is otherwise unchanged. E.g. def Foo = int; -> alias Foo = int;distinct has been replaced by typedef, syntax is otherwise unchanged. E.g. distinct Bar = int; -> typedef Bar = int;? instad of !. E.g. int! a; -> int? a;faultdef: fault MyFault { ABC, FOO_FAULT } -> faultdef ABC, FOO_FAULT;anyfault type is now called fault. E.g. anyfault the_err = @catch(foo); -> fault the_err = @catch(foo);Foo { 1, 2 } is no longer valid. Use C-style (Foo) { 1, 2 } instead.{| |} expression blocks have been removed. Use do { ... }; with break instead.&ref and $varef have been removed. Use #expr and $vaexpr instead.$vaarg(1) syntax is now invalid. Use $vaarg[1] instead..ordinal instead.$or, $and are removed. Use ||| and &&&.$concat is removed. Use +++ instead..allocator = allocator syntax for named arguments in calls is removed. Use the new syntax allocator: allocator instead.foo.type then explicitly do the cast instead.if (catch foo) { case ... } syntax is removed. Use if (catch err = foo) { switch (err) { ... } } instead.$foreach, $for and $switch no longer uses (). Remove the parentheses and end the statement with :, e.g. $foreach ($x, $y : $foo) -> $foreach $x, $y : $foo:int[?] syntax which was tested as an alternative to int[*].@return! in contracts are changed to @return?: before the description. E.g. @require a > b "a must be greater than b" -> @require a > b : "a must be greater than b".alias: def Foo = int @public; -> alias Foo @public = int;attrdef as the keyword rather than def.tmem rather than allocator::temp().mem rather than allocator::heap().new_init and other new_* functions have been removed. Use init(mem), s.concat(mem, s2) etc.@pool_init() to set up a temporary memory pool on a thread.@pool() no longer requires (or permits) an allocator. The temp allocator supports any number on in-flight temp allocators.Overall, the new functions, which implicitly used the heap allocator, has gone away and been replaced
by a function that explicitly takes the allocator. There are often also temp allocator variants, which
are then prefixed with t, e.g. tconcat.
Try to use the temp allocator when possible.
There are not only removals and changes, there are also some language additions and improvements in 0.7.0 compared to 0.6.8.
foo.xy += { 1, 2 }@format attribute has been added to compile time check formatting strings.!!foo used to not work, now it properly is interpreted as !(!foo)..ordinal has the same behaviour as associated values. This makes enums more flexible and closer to C enum behaviour.@wstring, @wstring32, @char32 and @char16 macros were added for interfacing with wide character libraries.The breaking changes of 0.7.0 are of two categories: (1) removals (2) syntax changes.
One might ask why one would remove things from a language? Surely more is better?
The reason is that learning a language this “one more feature”, is additional effort to learn the language. It’s also one more thing for people knowing the language to remember. In this way, each feature has a cost outside of compiler complexity.
When people encounter a rare feature such as the any-switch, they will need to look up how it works. This both takes time and undermines people’s confidence that they know the language.
So during the development of a language it’s important to weed out things that aren’t carrying its weight, and this is why 0.7 removes features.
As an example, int! a; has been used in C3 since the introduction of optionals. Why change it now?
In this case it’s because the semantics of int! a; was quite different from an optional when it was introduced.
The use of ! was exactly not to confuse it with an optional. Now it’s so much closer to the “common” optional that
a change to int? a; makes perfect sense.
def -> alias follows a similar trajectory. Initially it would be used both for aliases and to define new
attributes and distinct types. Then distinct type definition got its own separate definition, so it was more like
an alias aside from the attributes. With attributes getting its own attrdef, what remained were only aliases, and
changing the name to alias suddenly made perfect sense.
Both these changes make the language follow more common syntax conventions as well, which is a win.
The goal is for the syntax to feel straightforward and have as little “oh this is odd” as possible. This makes it both easier to learn and remember.
Even though neither alias nor int? exist in C, they’re familiar from adjacent languages. I believe
this will make C3 feel “more like C”, even though the changes are not in C.
Similarly, removing novel constructs like the expression block reduces the divergence from C syntax.
Where C3 0.7.0 has been a big cleanup release, it doesn’t have that many improvements to general quality
of life aside from the @format attribute.
I am really looking forward to inlining and resolving more function contracts on the caller side. In the first pass it will catch some low hanging fruit such as passing invalid constants, but this can be gradually improved to catch even more bugs.
Another thing is improving the standard library. There are additions I’d like to do both to the collections and to threads.
There are also a number of minor improvements to the compiler that would add to the quality of life for some uses. On top of this the inline asm is in need of bug fixes and cleanup.
Outside of the language and the compiler, a good docgen is needed.
But as for language changes, there is nothing planned, just incremental improvements to what’s already there.
Here is the full changelist for 0.6.8 to 0.7.0. Obviously if we’d bundle all changes from 0.6.1 to 0.7.0 the list would be MUCH bigger :D
Foo { 1, 2 } initializer.Foo(<int>) to Foo {int}.{| |} expression blocks.&ref and $varef parameters.$vaexpr(0) syntax in favour of $vaexpr[0]void! for main, test and benchmark functions.$or, $and, $concat compile time functions.@adhoc attribute.List{List{int}}..allocator = allocator syntax for functions.@operator(construct).abc.xz += { 5, 10 };$$wstr16 and $$wstr32 builtins.$foreach ”()” replaced by trailing ”:” $foreach ($x, $y : $foo) -> $foreach $x, $y : $foo:$for ”()” replaced by trailing ”:” $for (var $x = 0; $x < FOO; $x++) -> $for var $x = 0; $x < FOO; $x++:$switch ”()” replaced by trailing ”:” $switch ($Type) -> $switch $Type:$switch requires trailing ”:” $switch -> $switch:@return! to @return? and change syntax to require ”:” after faults.if (catch foo) { case ... } syntax.[?] syntax.int! to int? syntax.fault declaration using faultdef.foo ?? io::EOF with missing ’?’ #2036@public import recursive. #2018std::io::EOF is rendered as io::EOF.def to alias.distinct -> typedef.alias.LANGUAGE_DEV_VERSION env constant.anyfault -> fault.!!foo now works same as as ! ! foo.@stack_mem(1024; alloc) { ... };@pool_init() to set up a temp pool on a thread. Only the main thread has implicit temp pool setup.tmem is now a variable.--lsp #2058.@format attribute for compile time printf validation #2057."+".to_float() would panic.import can now both be @public and @norecurse.Foo[2] when Foo is distinct #2042.$i[$x] = $z.new_* functions in general moved to version without new_ prefix.string::new_from_* changed to string::from_*.String.to_utf16_copy and related changed to String.to_utf16.String.to_utf16_tcopy and related changed to String.to_temp_utf16mem::temp_new changed to mem::tnew.mem::temp_alloc and related changed to mem::talloc.mem::temp_new_array changed to mem::temp_array.ONHEAP variants for List/HashMap for initializing global maps on the heap..length_sq() with sq_magnitude()hash function.@wstring, @wstring32, @char32 and @char16 compile time macros added.Atomic to handle distinct types and booleans.math::iota.@pool no longer takes an argument.Allocator interface removes mark and reset.BackedArenaAllocator which is allocated to a fixed size, then allocates on the backing allocator and supports mark/reset.AnyList now also defaults to the temp allocator.os::getcwd and os::get_home_dir requires an explicit allocator.file::load_new and file::load_path_new removed.os::exit and os::fastexit added.Check out the documentation or download it and try it out.
Have questions? Come and chat with us on Discord.