The 0.7.0 milestone reached
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 (https://www.twitch.tv/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 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
Migrating 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 being deprecated)
Syntax changes
- Generics now use
{}
rather than(<>)
. E.g.List(<int>) x;
->List {int} x;
- The keyword
def
has been replaced byalias
, syntax is otherwise unchanged. E.g.def Foo = int;
->alias Foo = int;
- The keyword
distinct
has been replaced bytypedef
, syntax is otherwise unchanged. E.g.distinct Bar = int;
->typedef Bar = int;
- Optional types are now declared with
?
instad of!
. E.g.int! a;
->int? a;
- Declare faults with
faultdef
:fault MyFault { ABC, FOO_FAULT }
->faultdef ABC, FOO_FAULT;
- The
anyfault
type is now calledfault
. E.g.anyfault the_err = @catch(foo);
->fault the_err = @catch(foo);
- Compound literal syntax
Foo { 1, 2 }
is no longer valid. Use C-style(Foo) { 1, 2 }
instead. {| |}
expression blocks have been removed. Usedo { ... };
withbreak
instead.&ref
and$varef
have been removed. Use#expr
and$vaexpr
instead.$vaarg(1)
syntax is now invalid. Use$vaarg[1]
instead.- Casting an enum to its ordinal is no longer valid.
.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 syntaxallocator: allocator
instead.- The "any-switch" has been removed. Switch on
foo.type
then explicitly do the cast instead. if (catch foo) { case ... }
syntax is removed. Useif (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:
- Remove the
int[?]
syntax which was tested as an alternative toint[*]
. @return!
in contracts are changed to@return?
- Contracts that have a trailing description, now MUST have a
:
before the description. E.g.@require a > b "a must be greater than b"
->@require a > b : "a must be greater than b"
. - Order of attribute declaration is changed for
alias
:def Foo = int @public;
->alias Foo @public = int;
- Definition of new attributes use
attrdef
as the keyword rather thandef
.
Stdlib changes
- Use
tmem
rather thanallocator::temp()
. - Use
mem
rather thanallocator::heap()
. - The
new_init
and othernew_*
functions have been removed. Useinit(mem)
,s.concat(mem, s2)
etc. - The temp allocator is not implicitly set up on other than the main thread. Use
@pool_init()
to set up a temporary memory pool on a thread. - Lists and maps no longer allocate on the heap when not initialized. Instead, they use the temp allocator if not initialized.
@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.
Additions and improvements
There are not only removals and changes, there are also some language additions and improvements in 0.7.0 compared to 0.6.8.
- Swizzling assignment is now possible, e.g.
foo.xy += { 1, 2 }
- A
@format
attribute has been added to compile time check formatting strings. - Enum associated values can reference the calling enum.
!!foo
used to not work, now it properly is interpreted as!(!foo)
.- Enum inline values were already in 0.6.8, but from 0.7.0 the
.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.
Thoughts on 0.7.0
The breaking changes of 0.7.0 are of two categories: (1) removals (2) syntax changes.
Why remove things?
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.
Why change syntax?
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.
More like C
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.
C3 0.7.1 and onwards
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.
Full changelist for 0.7.0
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
Changes / improvements
- Removed
Foo { 1, 2 }
initializer. - Changed
Foo(<int>)
toFoo {int}
. - Removed
{| |}
expression blocks. - Removed macro
&ref
and$varef
parameters. - Removed
$vaexpr(0)
syntax in favour of$vaexpr[0]
- Enum does not cast to/from an integer (its ordinal).
- Removed use of
void!
for main, test and benchmark functions. - Removed
$or
,$and
,$concat
compile time functions. - Removed
@adhoc
attribute. - Disallow inline use of nested generics (e.g.
List{List{int}}
. - Remove
.allocator = allocator
syntax for functions. - Remove
@operator(construct)
. - Removal of "any-switch".
- Allow swizzling assign, eg.
abc.xz += { 5, 10 };
- Added
$$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:
- Empty
$switch
requires trailing ":"$switch
->$switch:
- Rename
@return!
to@return?
and change syntax to require ":" after faults. - Remove
if (catch foo) { case ... }
syntax. - Remove
[?]
syntax. - Change
int!
toint?
syntax. - New
fault
declaration usingfaultdef
. - Enum associated values can reference the calling enum.
- Improve error message on
foo ?? io::EOF
with missing '?' #2036 - Make
@public
import recursive. #2018 - Fault nameof prefixes the first last module path, for instance
std::io::EOF
is rendered asio::EOF
. - Rename
def
toalias
. - Change
distinct
->typedef
. - Order of attribute declaration is changed for
alias
. - Added
LANGUAGE_DEV_VERSION
env constant. - Rename
anyfault
->fault
. !!foo
now works same as as! ! foo
.- Temp allocator now supports more than 2 in-flight stacks.
- Printing stacktrace uses its own temp allocator.
- Allow inferred type on body parameters. E.g.
@stack_mem(1024; alloc) { ... };
- Use
@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.- Compile test and benchmark functions when invoking
--lsp
#2058. - Added
@format
attribute for compile time printf validation #2057. - Formatter no longer implicitly converts enums to ordinals.
Fixes
- Fix address sanitizer to work on MachO targets (e.g. MacOS).
- Post and pre-decrement operators switched places for vector elements #2010.
- Aliases were incorrectly considered compile time constants.
- FreeBSD libc stat definitions were incorrect.
- Atomic max was incorrect.
"+".to_float()
would panic.import
can now both be @public and @norecurse.- Crash when trying to convert a struct slice to a vector #2039.
- Crash resolving a method on
Foo[2]
whenFoo
is distinct #2042. - Bug due to missing cast when doing
$i[$x] = $z
. - Incorrectly allowed getting pointer to a macro #2049.
- &self not runtime null-checked in macro #1827.
- Bug when printing a boolean value as an integer using printf.
- Show error when a generic module contains a self-generic type.
- "Single module" was not enforced when creating a static library using as a project target.
Stdlib changes
new_*
functions in general moved to version withoutnew_
prefix.string::new_from_*
changed tostring::from_*
.String.to_utf16_copy
and related changed toString.to_utf16
.String.to_utf16_tcopy
and related changed toString.to_temp_utf16
mem::temp_new
changed tomem::tnew
.mem::temp_alloc
and related changed tomem::talloc
.mem::temp_new_array
changed tomem::temp_array
.- Add
ONHEAP
variants for List/HashMap for initializing global maps on the heap. - Remove Vec2 and other aliases from std::math. Replace
.length_sq()
withsq_magnitude()
- Change all hash functions to have a common
hash
function. @wstring
,@wstring32
,@char32
and@char16
compile time macros added.- Updates to
Atomic
to handle distinct types and booleans. - Added
math::iota
. @pool
no longer takes an argument.Allocator
interface removesmark
andreset
.- DynamicArenaAllocator has changed init function.
- Added
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
andos::get_home_dir
requires an explicit allocator.file::load_new
andfile::load_path_new
removed.os::exit
andos::fastexit
added.
If you want to read more about C3, check out the documentation: https://c3-lang.org or download it and try it out: https://github.com/c3lang/c3c