2025-07-05
This month’s release is 0.7.3, and is about gradual improvements to the language. Some parts of a language will always have less use than others, and that is true for C3 as well. For C3 it’s mostly the compile time parts that reveal their rough edges fairly late, but they need polish as well.
Types can be introspected an manipulated in two ways in C3. One is to just use the type directly, like in Type.sizeof
. The other is to use the typeid
value, to get the same thing: Type.typeid.sizeof
. However, they have differences: a typeid
value can never be in the position of a type, and only compile time constant typeid
has the full range of values:
typeid t = int.typeid;const typeid T = int.typeid;...// Getting the size, this works for allint.sizeof; // Compile time valueT.sizeof; // Compile time valuet.sizeof; // Runtime value// But others doesn'tint.max; // Direct type access ok!T.max; // Const typeid access ok!t.max; // ERROR! Runtime access is not ok!// Using it as a type in an assignment:int a; // Ok of courseT a; // ERROR!$typefrom(T) a; // Ok! Converts a typeid to a type.$typefrom(t) a; // ERROR! Must be a compile time typeid
For macros that had type arguments they usually looked like this:
macro new_thing($Type) { ... }...
Foo* f = new_thing(Foo);
The disadvantage was that if you had a constant typeid you’d first have to convert it to a type:
const typeid T = Foo.typeid;
void* thing = new_thing($typefrom(T));
Starting from 0.7.3 though, we can use the constant typeid directly and get an implicit conversion to a type:
const typeid T = Foo.typeid;
void* thing = new_thing(T); // New in 0.7.3
The reason this is useful, is because constant macros can’t return a type directly BUT can return a typeid. So maybe you have macro code like this:
typeid $type = @select_the_type($foo);
void* thing = new_thing($typefrom($type));
It can now be this instead:
typeid $type = @select_the_type($foo);
void* thing = new_thing($type);
And contracting the code it’s even more of an improvement:
void* thing = new_thing($typefrom(@select_the_type($foo))); // 0.7.2void* thing = new_thing(@select_the_type($foo)); // 0.7.3
It’s just overall clearer to drop the superfluous $typefrom
.
This also extends to the $assignable
builtin, which also now takes either a type or a typeid.
$evaltype
Also related to typeid and types is the deprecation of $evaltype
. It takes
a constant string and turns it into a type, so you could write:
String $name = "foo";$evaltype($name) x = 1;
In order to simplify the language $evaltype
is getting removed but the functionality is still there, because $typefrom
can now also take a string.
String $name = "foo";$typefrom($name) x = 1; // New in 0.7.3
This hopefully reduces the amount of different constructs it’s necessary to know in order to understand advanced compile time code generation.
C3 has type inference for initializers:
fn void test(int[3] x){ ... }
test({1, 2, 3});
However, this did not extend to &&
:
fn void test(int[3]* x){ ... }
test(&&{1, 2, 3}); // ERROR!
This has now been improved:
fn void test(int[3]* x){ ... }
test(&&{1, 2, 3}); // Works in 0.7.3
@sprintf
Creating strings at compile time is sometimes desirable, but the help for it has been complicated an inefficient. Usually the use of +++
has been needed:
String $s = $foo +++ ":" +++ $bar;
Now, however, there is a @sprintf
. It’s not a full-fledged printf-like function. It only supports “%s” arguments, but will do a best effort to convert any compile time value:
int $v = 123;Enum $x = FOO;String $s = @sprintf("%s:%s", $v, $x); // $s = "123:FOO"
Often this is good enough, and the operation is much cheaper than the other compile time concatenation in terms of compiler resources.
$assert
and $error
“printf”Hand-in-hand with the @sprintf
, $assert
and $error
now accepts the same format as @sprintf
for better compile time errors:
var $a = -123;$assert $a > 0 : "$a was %s", $a;// Above is a compile time error printing "$a was -123"
$eval
Where $evaltype
is going away, $eval
, which can turn a string into an identifier has been improved to properly work with @foo
, #foo
and $foo
identifiers.
The main
function used to only support String[]
or int argc, char **argv
as parameters. Now it accepts distinct types that lowers to the same arguments, so int argc, ZString* argv
works properly.
--sources
to add additional files to compileSometimes it’s useful to add extra sources to compile with a regular build. --sources
allows you to do exactly that:
c3c build --sources inject.c3
One omission of the arithmetic overloads was supporting macros with wildcard types, so rather than writing a macro Foo Foo.add(self, int x) @operator(+)
for every type you wanted to support, what if you could write macro Foo Foo.add(self, x) @operator(+)
and handle the rest with contracts and compile time checks?
This actually crashed in 0.7.2, but in 0.7.3 this works correctly. It’s even allowed with @operator_r
and @operator_s
variants.
Bitstructs can now be based on a distinct type, created by typedef:
typedef Test = int;bitstruct Foo : Test // Works in 0.7.3{ int z : 1..3;}
And it’s now possible for generics to be generic over distinct const values:
module foo { VAL };const GLOBAL = VAL;
module test;import foo;typedef Bar = int;
fn void main(){ const Bar B = 123; Bar b = foo::GLOBAL{B}; // Works in 0.7.3}
Vectors has been comparable at runtime, but not at compile time. However, from 0.7.3 and onwards this works:
int[<2>] $v = { 1, 2 };int[<2>] $w = { 1, 2 };$assert $v == $w;
@deprecated
has been mostly used as a migration mechanism for the c3 standard library, but using it with user code requires more fine-grained control, which is why there is now an @allow_deprecated
attribute to silence deprecation warnings inside a particular function:
fn void old() @deprecated {}
fn void test1(){ old(); // Emits warning}
fn void test2() @allow_deprecated{ old(); // No warning}
'\f'
as whitespace\f
(the form-feed character) is not really used in normal text, so it has been removed from the characters recognized as whitespace by the lexer.
$memberof
get/set improvementsYou can get the member of a struct or union by indexing Type.membersof
and then use the get
method on the member to retrieve the value. However, there was no corresponding set
method, instead you had to do *&$member.get(2) = 123
. People have been asking for a .set
for a long time, and that was also the only way to properly support setting bitstruct values:
bitstruct Foo : int{ int x : 1..5; int y : 10..14;}fn void test(){ var $member = Foo.membersof[1]; Foo f = { 1, 3 }; int val = $member.get(f); // .get on bitstructs now works $member.set(f, val + 1); // .set method updates io::printn(f.y); // Prints 4}
It’s now possible to override the extension used for libraries and executables of a target with the "extension"
project property.
0.7.3 shipped with around 40 bug fixes, which is a bit below average for a 0.0.1 release.
ZString types can now be compared using ==
by having equality overloaded. We have a SHA512 library and String.escape
, String.unescape
for escaping and unescaping strings. io::struct_to_format
also now supports bitstructs, which means io::printn
and related functions properly prints bitstructs.
String.is_zstr
and String.quick_zstr
are unsafe and are deprecated is_array_or_slice_of_char
and is_arrayptr_or_slice_of_char
are replaced by constant @
variants.
$typefrom
now also accepts a constant string, and so works like $evaltype
.$evaltype
is deprecated in favour of $typefrom
.-0xFF
now a signed integer.$Type
assignment, and $assignable
.foo.#bar
.&&
#2172.$eval
now also works with @foo
, #foo
, $Foo
and $foo
parameters #2114.@sprintf
macro (based on the $$sprintf
builtin) allows compile time format strings #1874.$assert
and $error
#2183.main
a bit more liberal, accepting main(int argc, ZString* argv)
$echo
and @sprintf
correctly stringify compile time initializers and slices.--sources
build option to add additional files to compile. #2097$Foo = int
) is no longer an expression.@allow_deprecated
attribute to functions to selectively allow deprecated declarations #2223.-2147483648
, MIN literals work correctly.$define
handling of binary ops.--lsp
sometimes does not emit end tag #2194.@format
checking #2199.unreachable()
only panic in safe mode.cflags
additions for targets was not handed properly. #2209$echo
would suppress warning about unreachable code. #2205is_array_or_slice_of_char
#2214.+=
compile incorrectly #2217.$defined(#expr)
broken with binary. #2219x++
and x--
works on pointer vectors #2222.x += 1
and x -= 1
works properly on pointer vectors #2222.x += { 1, 1 }
for enum and pointer vectors #2222.@public
#2224.math::overflow_*
wrappers incorrectly don’t allow distinct integers #2221.String.is_zstr
and String.quick_zstr
#2188.==
for ZString types.is_array_or_slice_of_char
and is_arrayptr_or_slice_of_char
are replaced by constant @
variants.@pool
now has an optional reserve
parameter, some minor changes to the temp_allocator APIString.escape
, String.unescape
for escaping and unescaping a string.Check out the documentation or download it and try it out
Have questions? Come and chat to us on Discord.