The C3 Blog

C3 0.7.3 - Small Improvements

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.

Type / typeid

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 all
int.sizeof; // Compile time value
T.sizeof; // Compile time value
t.sizeof; // Runtime value
// But others doesn't
int.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 course
T 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.2
void* 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.

Removing $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.

Improved type inference

C3 has type inference for initializers:

fn void test(int[3] x)
{ ... }
test({1, 2, 3});

However, this did not extend to &&:

0.7.2
fn void test(int[3]* x)
{ ... }
test(&&{1, 2, 3}); // ERROR!

This has now been improved:

0.7.3
fn void test(int[3]* x)
{ ... }
test(&&{1, 2, 3}); // Works in 0.7.3

Compile time @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"

Better $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.

More liberal “main”

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 compile

Sometimes it’s useful to add extra sources to compile with a regular build. --sources allows you to do exactly that:

Terminal window
c3c build --sources inject.c3

Improved overloading with wildcard types

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.

Distinct improvements

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
}

Constant vector comparison

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;

Silence deprecation

@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
}

Removal of '\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 improvements

You 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
}

Custom file extensions in build targets

It’s now possible to override the extension used for libraries and executables of a target with the "extension" project property.

Bug fixes

0.7.3 shipped with around 40 bug fixes, which is a bit below average for a 0.0.1 release.

Stdlib additions

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.

Stdlib deprecations

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.

Change Log

Click for full change log

Changes / improvements

  • $typefrom now also accepts a constant string, and so works like $evaltype.
  • $evaltype is deprecated in favour of $typefrom.
  • Literal rules have changed, this makes -0xFF now a signed integer.
  • Implicitly convert from constant typeid to Type in $Type assignment, and $assignable.
  • Make $Type parameters accept constant typeid values.
  • Deprecate foo.#bar.
  • Allow inference across && #2172.
  • Added support for custom file extensions in project.json targets.
  • $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.
  • Improve error reports when encountering a broken “if-catch”.
  • Add printf format to $assert and $error #2183.
  • Make accepting arguments for main a bit more liberal, accepting main(int argc, ZString* argv)
  • Make $echo and @sprintf correctly stringify compile time initializers and slices.
  • Add --sources build option to add additional files to compile. #2097
  • Support untyped second argument for operator overloading.
  • The form-feed character ‘\f’ is no longer valid white space.
  • Show code that caused unreachable code #2207
  • Allow generics over distinct types #2216.
  • Support distrinct types as the base type of bitstructs. #2218
  • Add hash::sha512 module to stdlib. #2227
  • Compile time type assignment (eg $Foo = int) is no longer an expression.
  • Add @allow_deprecated attribute to functions to selectively allow deprecated declarations #2223.
  • Improve error message on pointer diff #2239.
  • Compile-time comparison of constant vectors. #1575.
  • $member.get supports bitstructs.
  • $member.set for setting members without the *& trick.
  • Initial support for #1925, does not affect C compilation yet, and doesn’t try to link etc. Using “—emit-only”

Fixes

  • -2147483648, MIN literals work correctly.
  • Splatting const slices would not be const. #2185
  • Fixes to $define handling of binary ops.
  • Fixes methodsof to pick up all sorts of extension methods. #2192
  • --lsp sometimes does not emit end tag #2194.
  • Improve Android termux detection.
  • Update Android ABI.
  • Fixes to @format checking #2199.
  • Distinct versions of builtin types ignore @operator overloads #2204.
  • @operator macro using untyped parameter causes compiler segfault #2200.
  • Make unreachable() only panic in safe mode.
  • cflags additions for targets was not handed properly. #2209
  • $echo would suppress warning about unreachable code. #2205
  • Correctly format ‘%c’ when given a width. #2199
  • Fix to is_array_or_slice_of_char #2214.
  • Method on array slice caused segfault #2211.
  • In some cases, the compiler would dereference a compile time null. #2215
  • Incorrect codegen if a macro ends with unreachable and is assigned to something. #2210
  • Fix error for named arguments-order with compile-time arguments #2212
  • Bug in AST copying would make operator overloading like += compile incorrectly #2217.
  • $defined(#expr) broken with binary. #2219
  • Method ambiguity when importing parent module publicly in private submodule. #2208
  • Linker errors when shadowing @local with public function #2198
  • Bug when offsetting pointers of large structs using ++ and —.
  • x++ and x-- works on pointer vectors #2222.
  • x += 1 and x -= 1 works properly on pointer vectors #2222.
  • Fixes to x += { 1, 1 } for enum and pointer vectors #2222.
  • Linking fails on operator method imported as @public #2224.
  • Lambda C-style vaargs were not properly rejected, leading to crash #2229.
  • Incorrect handling of constant null fault causing compiler crash #2232.
  • Overload resolution fixes to inline typedef #2226.
  • math::overflow_* wrappers incorrectly don’t allow distinct integers #2221.
  • Compiler segfault when using distinct type in attribute imported from other module #2234.
  • Assert casting bitstruct to short/char #2237.
  • @tag didn’t work with members #2236.
  • Assert comparing untyped lists #2240.
  • Fix bugs relating to optional interface addr-of #2244.
  • Compiler null pointer when building a static-lib with -o somedir/… #2246
  • Segfault in the compiler when using a bitstruct constant defined using a cast with an operator #2248.
  • Default assert() message drops parens #2249.

Stdlib changes

  • Deprecate String.is_zstr and String.quick_zstr #2188.
  • Add comparison with == 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 API
  • io::struct_to_format now supports bitstructs.
  • Add String.escape, String.unescape for escaping and unescaping a string.

Want To Dive Into C3?

Check out the documentation or download it and try it out

Have questions? Come and chat to us on Discord.