Skip to content

The C3 Blog

If You Have Users, You Have to Market Your Programming Language

I don't like to think about marketing. I think few programmers do. We tend to dream of a “build it and they will come” situation, where if we just make something great then it will be picked up and get the recognition it deserves. But real life doesn't work like that.

We know of what's popular and that occupies most of our attention. It is a useful evolutionary trait: often what's popular is popular because it managed to out-compete the alternatives, so it saves us from diving into details for everything.

Also, by virtue of being popular it falls under greater scrutiny, so where something less used might have problems few know about, the popular thing's downsides will usually be well explored. It might not be the best but it communicates a lot, just being popular.

The fact that something or someone has the money/influence to do marketing is in itself a signal of quality: you don't get money/influence without having demonstrated some kind of success. And you get successful by doing something right. Marketing something you don't believe would sell would mean you're making the wrong decision, which you then – by virtue of being successful – have fewer reasons to do. This boils down to the message that someone spending money on marketing, on average, means that the product is good!

This doesn't mean that only good products are marketed of course! But it is what it strongly signals to us. That's why we respond to it even if we don't resonate with the marketing message itself. Without digging into the subject too deeply: just keep in mind that popularity and marketing shape people's opinions on many levels. It creates trust.

Now let's get to the main point: programming languages and marketing. I'm not going to talk about how to market programming languages or what's difficult about it. No, I want to talk about the effects of marketing, or not marketing your language.

People have different goals: it might be a hobby project for their own consumption or ...ahem... a way to world domination in the domain of programming 😄. It might be reasonable to think that marketing isn't needed for the former, but what I'd like to demonstrate is that the case is not clear-cut.

For the sake of argument, let's say I neither go to the extreme of “the goal is to make this language the most popular in the world” nor “only I will ever lay my eyes on this language”. If so, you're building a language for yourself and others to use. – You might tell yourself: “it doesn't matter if this becomes popular, it's enough that I can use it”. This seems to be a pretty common attitude: trying to be realistic about success without closing the door to success. I'm going to tell you this is the wrong thing to do. You should try to make it popular no matter what!

We need to approach this at an uncommon angle: Let's say you have a few people also using your language, what value are you providing them? The language certainly, and usually the toolchain and standard library.

Now, what does the quality of those tools depend on? Roughly: (1) the amount of time people dedicate to them (2) the quality of the effort (3) how much testing has gone into them.

No matter how good your language is, no matter how well you've selected features for the standard libraries for the toolchain: the more people you have using your language, the more people you will have testing it (but the more people, the fewer bugs a single user will encounter!), finding missing functionality in your standard library, or just finding ways the toolchain fails on their particular setup.

What editor are you using? It might be reasonable that you'd make sure that this one has syntax highlighting, but what about all the many different editors people use? Editors you might not even have heard of?

Tutorials? You might do great tutorials, but they will be from your point of view, for tasks you think are relevant.

Even if your users produce these for themselves, the extent of how those tools are tested also depends on the number of users. We can look at anything: platform support, library bindings, sample projects. Your users will benefit the more users there are. It will just be a better experience and raise the value of everything your users do: if they make a library, they will know others will use it, same for editor syntax highlighting, tutorials and bindings. This is a virtuous cycle where everyone can benefit.

This also explains why people get invested in their language of choice and might become zealous in their promotion of it. We can't just dismiss it with people being “cultish”: they're actually sometimes simply doing what is in their power to improve the value in their favorite investment: it is their way to try bringing better tooling, libraries and other things to the language they enjoy.

Tsoding once remarked:

“Majority of people treat Programming Languages as some sort of Cryptocurrencies.”

For new languages this is a fairly accurate description of what's happening. Users are aware on some level that more focus on their language of choice will mean a better experience using that language.

We might not like this, but it is what it is.

While your users can contribute, they can't decide on the central marketing message. This is intrinsically tied to the author of the language. If the author says “I don't care if it just remains a hobby project” that might seem self-deprecating and humble. But what it means in practice is: “I don't care about providing value to my users”.

It's the “build it and they will come”. Well your users arrived and you thought that was the end of your responsibility. All you need to do is just make the language really good and your job is done. Right? Except your users put effort into learning and contributing, and you're wasting this potential. “I'm fine with it being a hobby project” is the excuse. Your language is abandonware. It's not humility, it's failing to understand your obligations.

Marketing is not about making your language “win”. It's not about aggrandisement. It's about fulfilling the obligation to your users, the ones who liked what you did, and had the courtesy of investing time in using it. Your language might never catch on, that is fine, but there is a difference between trying and failing, and not trying at all. If you don't do it, you can hardly expect them to fork the compiler and continue the work. You've shown the world you don't think the language is good enough to market. That's a signal too, a very strong one.

So language designers: we should either make it clear that something is just a toy language which isn't going anywhere – or we should take responsibility and do the marketing as much as we can. Do or do not, there is no try.


Discuss this article on Reddit and Lobste.rs.

I thought I was building a C replacement. I was wrong

I made a rather fundamental mistake when I started marketing C3.

I called it a C alternative.

That seemed completely obvious to me. I had been looking for a better C for years, I found C2 to contribute to, and from that C3 was eventually born. Of course C3 was a C alternative.

But I've gradually realized that the phrase "C alternative" means something very different today from what it meant to me.

And the reason is embarrassingly simple: I'm old enough to remember when C was an application language. So to understand what I got wrong, we have to go back to the 80s and 90s.

I started in BASIC, and for the longest time, assembly and BASIC were the main options. This was during the home computer "revolution" of the 80s. With the advent of 16-bit home computers, more languages suddenly became viable — mainly Pascal and C.

At that time, the most difficult thing was actually finding a compiler if you were young and had hardly any budget. I even ended up doing serious programming in QBasic of all things, simply because it was bundled with my install of DOS! And later I ... ehem ... "got hold" of a copy of Turbo Pascal...

My Pascal days were ultra productive. If I wanted to do something, it was just a matter of sitting down and writing it. Compared to writing things with labyrinthine BASIC goto/gosub, Pascal was super nice and easy to organize. This was easy without imposing structure or architecture up front – after all, it was procedural.

I did learn a smattering of C by getting hold of GCC, and later did some introductory C++ at university. And at this point, everything I wrote on my own was procedural in style. Then I had a summer course in Java. I think this might have been 1996 and the internet was all new.

And Java introduced OO as the core approach.

It was novel, it was interesting, and despite having written C++, I felt I hadn't really understood what OO was about until then. So that started my love affair with OO. Not that it was needed that much initially. Even getting a job in C++ later on gave me little room to do OO. But let's fast forward.

I ended up being exposed to Objective-C, and a job on Java game servers gave me a massive amount of practice writing Java quickly and well, but I still felt a nagging dissatisfaction. Because I never really got the raw development speed I had in my early days with Turbo Pascal, I was much better at programming, I was somehow slower – thinking much more about design than before for equivalent code. And this was something I didn't really reflect on that until I had a long-term contracting gig doing PHP.

The codebase was somewhat OO, but not overly so. And the interesting thing was that, at its core, it was basically:

"The user made a call, route it to running this function and present this result."

Even if bits and pieces inside were wrapped in OO classes, it was mostly procedural. And by god, development was ruthlessly efficient.

Step by step it dawned on me that the OO parts were superfluous: – the whole thing could have been C with an arena allocator + good dynamic arrays and strings. No OO needed.

And that got me thinking. Here it was: the development speed of Turbo Pascal, so why didn't OO give me that? I discovered an answer in OOPs requirement of up-front architecture.

In OOP, you need to think about architecture from the first — what objects own what objects, what objects know of what objects. And a big part of the "best practices" in OO, such as programming to interfaces, are really ways of trying to mitigate the problem of this deep coupling.

The methods are fundamentally linked to this problem. As soon as we write:

foo.do_something(bar)

we have created a hierarchy where the class of foo is more fundamental than bar.

In programming, we talk about exploring the problem space as we develop a program. As we gain a deeper understanding, we will usually restructure the program so that it more easily solves the problem.

The problem with OOP — or, if I may spread the net wider, "methods first" — is that the up-front architecture and the use of methods inhibit these changes. When we have placed B in A, reversing that relationship doesn't just mean moving the field: it also means rewriting all the methods that rely on it. This makes us reluctant to do such refactorings, which in turn means that bad decisions made up front tend to get locked in.

This is not just a problem for OO, but for anything that is "methods first" — that is, when you think:

game.run()

instead of:

run(&game)

The latter is procedural thinking. The former is "methods first".

As an aside, C3 has methods because they are unreasonably effective for avoiding the need for function overloading in things like foo.to_string() But this also makes it harder to drag people out of the "method first" mindset.

(Here Odin has an advantage: it gets people into a good mindset from the start.)

For me, things were starting to come together.

My PHP experience showed me that the OO parts were never actually needed to create nice abstractions. And the realization that OO — or more broadly, "methods first" — was making it harder to write good programs with good architecture gave me a better understanding of what I had been looking for all along.

C wasn't quite there, because libc was extremely bare-bones and overall didn't have the necessary ergonomics anymore. And yet a subset of C++ wasn't the solution either. It was marinated in "methods first", but above all, it was so slow to compile.

So yes, I was looking for a C alternative.

But what I was actually looking for was a C alternative with the ergonomics that made it suitable for current-day application development. And that distinction turned out to matter a lot.

When I would say "C3 is a C-like for people who like C", I was thinking about people who would write everything in C. People like Sean Barrett.

But something I've gradually come to terms with is that "C-like" or "C alternative" today means something rather different. It means "a programming language for what C is predominantly used for today". That means OS development, embedded, and high-performance niche libraries and backends.

People write databases in C, not their next video editor or game. For that they use C++ or something like that. Because with C++, you get the added ergonomics C lacks: dynamic strings and arrays, maps, a sprinkle of overloading. And that makes perfect sense.

But that isn't what I was thinking about. I was thinking of C as a language for writing programs. General-purpose applications. The kinds of things people now reach for C++, Objective-C, Swift, Java or Kotlin to build.

I always thought of C3 as being just as nice — or better — to write those applications in. This is not because I wanted to make C3 into another C++, or another Java, or another Swift. Quite the opposite. I wanted the raw simplicity and performance of C, but with the ergonomics needed to make it pleasant to build modern software. Just a language you pick to get things done, with enough ergonomics to make that as nice and convenient as possible.

In the eyes of most programmers today, this is not what they associate with C. But it was what I associated with C, because I'm old enough to remember when that was indeed the role of C.

(And this is also why Zig's decisions are so different from C3's: Zig's primary goal seems to be to replace C where it's used today.)

When people would say "why do people want to replace C?", I mistakenly always took it as:

"C is good enough, we don't need a replacement."

But what they meant was really:

"C isn't used for anything interesting, so why bother with a replacement?"

And that's where I had misunderstood the discussion. I was using an old definition of "C alternative" in a world where C means something else. So when I marketed C3 as a C alternative, I made a pretty fundamental mistake.

C3 was always an alternative to anything from C to C++ to Swift. – I just never thought enough about how much the world had changed. Which changes how I need to talk about C3.

So moving ahead, C3 will talk less about being a C-like, and more about being a programming language for building general-purpose applications. Because the point was always:

refreshing C to make it a pleasant general-purpose application language again.

In other words, I spent years marketing C3 as a C replacement, but what I was really trying to build was something much broader.

I just marketed that wrong.


Discuss this blog post on Reddit, Lobste.rs or HackerNews.

C3 0.8.3 Feature flags

C3 0.8.3 is now available. The most notable change is the addition of the @feat attribute, which will be the new way to do top-level conditional compilation.

Language changes & improvements

@feat

This release adds the @feat attribute, which is replacing the older @if usage on non-generic top-level declarations. This attribute is essentially the same as @if($feature(...)) before 0.8.3, but top-level @if is now getting phased out except for use in generics.

The problem with @if

Before @if we had $if on the top level, mirroring C's #if and #ifdef. However, this gave very little information for the compiler when someone tried to use a declaration guarded by $if. So @if was introduced to give more granular support and to make it easier for IDEs to figure out "go-to definition" style functionality.

Unfortunately, @if still had the problem of $if, which is that its argument could be any arbitrary compile time constant expression. But what happens if we use something like @if(Foo::methods.len == 3)? Well, if the compiler hasn't yet reached a stage where Foo is analysed, its analysis is now forced. Furthermore, even if Foo is analysed, methods might not be registered yet, giving subtly wrong answers. Because C3 runs in multiple passes, there is no canonical ordering of which thing to analyse first, so it was quite unpredictable. Even though refinements were constantly made to the ordering, the underlying flaw was unsolvable.

The insight

Surveying the use of @if they roughly fell into one of three categories:

  1. Conditionally include depending on compilation target, e.g. @if(env::WIN32)
  2. Include "if no other implementation is found", e.g. @if(!$defined(String)).
  3. Conditionally add functionality depending on generic type, e.g. @if($defined((Type){} + (Type){}))

Of these, the (3) didn't pose a problem in most cases, because in general it happened fairly late. In the case of (2), this was now mostly covered by the new behaviour of @weak (introduced in 0.7.11).

So what about @if(env::WIN32)? This was almost completely confined to the std::core::env module, which would define constants and derived constants based on compiler builtins, and all of those cases could actually be directly passed in by the compiler.

Introducing @feat

As previously mentioned, @feat is essentially $if($feature(...)) but to make this work, the compiler now provides many feature flags built-in, such as WIN32, LINUX, BIG_ENDIAN etc. In general any use of env::CONSTANT has a @feat(CONSTANT) counterpart.

This solves the ordering problem because all these constants are known before declarations are registered, so any symbols with @feat can be filtered early. This is not only beneficial for the compiler – LSPs and editors can take advantage of this and accurately determine what's available at compile time.

In more detail, @feat may take multiple constants, in which case it is satisfied on any match:

fn void foo() @feat(WIN32, MACOS) // available on WIN32 and MACOS
{}

It's also possible to use | instead:

fn void foo() @feat(WIN32 | MACOS) // alternative syntax
{}

! may be used to negate a feature:

fn void foo() @feat(!WIN32) // available on anything other than WIN32
{}

Multiple @feat means all must be satisfied:

fn void foo() @feat(MACOS) @feat(AARCH64) // only on Aarch64 MacOS
{}

It's possible to use & as an alternative:

fn void foo() @feat(MACOS & AARCH64) // only on Aarch64 MacOS
{}

More complex expressions using | & and ! are allowed:

fn void foo() @feat((MACOS & AARCH64) | !LIBC) // On Aarch64 MacOS or not with libc
{}

Together with this the $feature function has been renamed $feat ($feature is now deprecated and will be removed in 0.9.0).

Possible keyword changes

0.7.0 introduced the family of "-def" keywords: constdef, typedef, faultdef and attrdef. After evaluating this for a little over a year, there has been some doubt whether these are really good enough. For this reason 0.8.3 introduces experimental aliases for each:

  • constdef -> constset, cenum
  • typedef -> distinct
  • faultdef -> faultconst, faultset, excuse
  • attrdef -> attrgroup, attrmacro

These are considered experimental, meaning that there is no stability guarantee. Versions 0.8.4 and onwards are free to remove these aliases.

Please take time to try them out and offer feedback on them.

Fetching MacOS

Previously, automated fetching of the Windows SDK was added. This version adds SDK fetching for MacOS as well, allowing completely effortless cross-compilation to MacOS from any other platform.

Windows Aarch64 support

With this version the C3 compiler finally supports Aarch64 as a Windows target.

Standard Library Updates

Regex

With this release, C3 finally has regex support. Please note that the API is considered experimental and may change in a later 0.8.x version.

Other changes

JSON/object unmarshaling is now available through json::unmarshal, object::unmarshal, and related functions. json::temp_load is deprecated in favor of json::tload.

Geometry code should note one behavior change: Rect.contains_point is now exclusive on the maximum edge. A new Bounds type has also been added for rectangular regions represented by min and max, with operations inclusive along the boundary edge.

LinkedList and Deque now have a prepend method, FixedList has is_full(), and ranges gained range::upto, range::inclusive, range::exclusive, Range.to_array, and ExclusiveRange.to_array.

Other useful additions include io::read_buffer and comparison operators for DateTime.

Finally, the tracking allocator is now able to track cross-thread allocation.

Fixes

This release fixes a wide range of correctness issues, including generics, compile-time execution, JSON parsing, IO streams, compression, networking, AES CTR, RISC-V ABI handling, doc generation, and several compiler crashes.

Notable fixes include:

  • Generic functions and values now correctly require a prefix.
  • The arena allocator no longer errors in safe mode when freeing the last memory while the arena is full.
  • LinkedList.push_front_all now preserves the expected order.
  • BitSet.len now reports the bit set length instead of the underlying type size.
  • RFC 3339 formatting now emits the correct value for microseconds.
  • Timed tcp::connect now returns the real result instead of always failing with io::GENERAL_ERROR.
  • Generic methods are no longer checked before their generic type is fully registered.
  • _erff now invokes C erff instead of erf.
  • AES CTR no longer loses sync for data whose size is not a multiple of 16.
  • JSON no longer accepts \v as whitespace.
  • JSONC parsing no longer loops indefinitely on unterminated comments.
  • io::read_all now handles split data.
  • Scanner now handles chunked data correctly.
  • Gzip readers can now handle streams without seek support.
  • io::printf("%3d", 1) now returns the correct printed length.
  • RISC-V structs with mixed floating-point and integer fields are now passed and returned by value correctly.

Doc generation also received several fixes: attrdef declarations and docs are emitted, alias doc comments are included, @return contracts are rendered, compiler_rt is excluded when --emit-stdlib=no, and empty JSON fields are omitted for slimmer output.

Thank yous

Again, 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.

PR contributors for this release

Stdlib: cmann1, Darvisim, hchac, Lado, Manu Linares, Omar Alani

Compiler & toolchain: Darvisim, Fernando López Guevara, Lado, LowByteFox, Manu Linares,

CI/Infrastructure: LowByteFox, Manu Linares, ttambow

Change Log

Click for full change log

Changes / improvements

  • Windows aarch64 is now supported.
  • Tracking allocator can now accept cross-thread allocations.
  • Filter test backtraces #3368
  • Improved GDB compatibility for macros.
  • Fail when "emcc" is unavailable instead of falling back to the built-in wasm linker.
  • Support fetching MacSDK for easy cross compilation.
  • Add @feat attribute, deprecate @if on non-generic top level declarations.
  • Add $feat compile time function. $feature is deprecated and replaced by $feat.
  • Experimental support for constset, cenum, faultconst, faultset, excuse, attrgroup, attrmacro, distinct.
  • Defer resolution of typedef alignment and generics, allowing more recursive definitions.
  • Improve error message on multiple < > in a row. #2971
  • Add the lgdt and lidt instructions to x86 inline assembly.

Stdlib changes

  • LinkedList and Deque added a prepend method.
  • Added FixedList.is_full() method
  • Rect.contains_point is now exclusive on the maximum edge.
  • Add Bounds - a rectangular region stored as a min and max value, with all operations being inclusive along the boundary edge.
  • Experimental regex support.
  • Improved RFC 3986 compatibility.
  • JSON unmarshaling support, json::unmarshal and family.
  • Object unmarshaling support, object::unmarshal and family.
  • json::temp_load deprecated in favour of json::tload.
  • Object::is_map now returns true for empty objects.
  • Add range::upto, range::inclusive and range::exclusive macros.
  • Add io::read_buffer.
  • DateTime enable comparison operators.
  • Add Range.to_array and ExclusiveRange.to_array methods.

Fixes

  • Generic functions and values incorrectly would not require a prefix. #3374
  • Arena allocator would error in safe mode when freeing the last memory and the arena was full. #3378
  • LinkedList push_front_all was appending in the wrong order.
  • BitSet.len would yield the size of the underlying type, not the length.
  • RFC3339 formatting would yield incorrect value for microseconds.
  • --obj will always retain the object files. #3380
  • ThreadGroup with function returning void was broken.
  • Timed tcp::connect always failed with io::GENERAL_ERROR instead of the real result.
  • Compile time struct with zeroed union member access causes compiler error #3382.
  • Generic methods checked before the generic type is fully registered.
  • Math function _erff invoked C erf function instead of erff function #3391
  • Defining local constants inside a macro causes it to fail to @const fold. #3397
  • AES CTR would lose sync on data not multiples of 16.
  • In some cases, on macros rethrowing optional values codegen could fail.
  • Json accepted incorrectly accepted \v as whitespace.
  • JSONC parsing on unterminated comments would loop indefinitely.
  • ZII array constdef would cause an assert. #3411
  • Calling a constant void macro inside a macro stops it from being constant. #3410
  • Using io::struct_to_format with $force_dump = true failed to compile.
  • $foo += 1 would not do a copy, leading to incorrect update of $foo. #3400
  • foreach (foo::Type t : x) would not parse properly. #3423
  • Docgen improvements and fixes: emit attrdef declarations/docs, include alias doc comments, render @return contracts, exclude compiler_rt when --emit-stdlib=no, and omit empty JSON fields for slimmer output #3422.
  • Several uses of InStream didn't properly handle io::EOF.
  • Fixes to memory handling during zip loading.
  • Multireader reading after a final empty read would crash.
  • RISC-V structs with mixed FP and integer fields were corrupted when passed or returned by value. #3428
  • io::read_all didn't handle split data.
  • Scanner would not correctly handle chunked data.
  • Gzip reader couldn't handle a stream without seek.
  • io::printf("%3d", 1) would return the wrong printed length. #3432
  • Crash when using ternary operator with vector type and inline constdef #3433
  • Crash on assign-op to compile-time subscript with non-const result #3419

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.

C3 0.8.2 A modest improvement

C3 0.8.2 is out. This release brings a batch of language ergonomics improvements — most notably reusable target templates for libraries and generic constdef — along with new reflection capabilities and a round of bug fixes.

Compared to 0.8.1, this is a rather modest release, but anyway, here's what's new:

Language changes & improvements

Reusable target configurations for libraries

Libraries can now expose a templates map in their manifest, and projects can pull one into a target with template: "library/template". Template properties load first and target-local settings override them, so libraries can ship sensible, shareable build configurations instead of asking every user to copy-paste setup.

Reflection for generics

Foo::is_generic(...), Foo::generic_qname, and Foo::generic_args finally lets you inspect generic instantiations.

alias Foo = List{int};

fn void main()
{
    typeid t = List{String}::generic_args[0]; //String::typeid
    String name = Foo::generic_qname; // "std::collections::list::List"
    bool x = Foo::is_generic(List); // true
    bool y = int::is_generic(List); // false
}

Bitstruct properties

Bitstruct members gain bitoffset and bitsize properties:

bitstruct Foo : uint
{
   uint a : 0..1;
   bool b : 6;
   bool c : 10;
   int d : 14..20;
}

fn void main()
{
    int a = $reflect(Foo.a).bitsize; // 2
    int b = $reflect(Foo.d).bitoffset; // 14
    int c = $reflect(Foo.c).bitoffset; // 10
}

New @param options: own, init and drop

It's now possible to annotate more than in/inout/out on parameters:

  1. own signifies that the function will in some way retain the variable beyond the scope of the function.
  2. drop means that the pointer and the data pointed to will be invalid when the function returns.
  3. init means that the data pointed to will be initialized by the function.

This can be used by the compiler for static analysis and also documents the function or macro.

<* 
 @param [own] f
*> 
fn void foo(Foo* f)
{
    some_global.foo = f;
}

Asm stack alignment

Before 0.8.2, all asm blocks would be stack aligned, which didn't match C behaviour. This is now fixed. Stack alignment is now opt-in by using @align:

asm // Unaligned
{
  syscall;
}

asm @align // Stack aligned
{
  syscall;
}

Windows improvements

The Windows subsystem can now be set directly. Windows can still use @winmain to implicitly set the Window subsystem, but all subsystems are now available, allowing for things like EFI applications.

Standard library

  • Atomic.compare_exchange added.
  • array::contains_slice and array::index_of_slice for substring-style searches over arrays.
  • log::register_dynamic_category, for libraries to define their own logging categories dynamically.
  • String.index_of and rindex_of now accept finding empty strings.

Fixes

Stdlib fixes

  • Denormal (subnormal) results are now handled correctly by String.to_double() — very small magnitudes near the floating-point floor previously came out wrong.
  • A float literal with an uppercase F suffix was incorrectly typed as a double instead of a float.
  • JSON serialization now correctly handles Unicode and \v (#3353).
  • IPv6 parsing: "a::b:c:d:e:f:0" is now accepted as valid.
  • A regression in BackedArenaAllocator when destroying it could cause a crash.
  • Fixed macOS regression which was breaking stack traces.

Compiler fixes

  • $stringify would sometimes wrap output in extra parentheses.
  • constdef vector with an alias was incorrectly lowered
  • The compiler asserted when concatenating a struct to an untypedlist
  • untypedlist was not detected as invalid as an enum associated-value type or as a pointer

Thank yous

Again, 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.

PR contributors for this release

Stdlib: AtomicSynth, Book-reader, GuineaPigUuhh and Manu Linares

Compiler & toolchain: Book-reader, Johannes Müller, Manu Linares, Osama Badeeb, rickyadastra and Rodrigo Camacho

CI/Infrastructure: Hade and Manu Linares

Change Log

Click for full change log

Changes / improvements

  • @weak now works with all declarations.
  • Add @align for asm blocks to stack align them. Stack alignment is no longer default.
  • Allow setting Windows subsystem directly.
  • Add bitoffset and bitsize reflection properties to bitstruct members. #3219
  • Improve error message on trying to cast char array to String. #3343
  • Add Foo::is_generic(...), Foo::generic_qname and Foo::generic_args. #2909 #3329
  • Add own, init and drop parameter annotations.
  • constdef can now be generic.
  • Libraries can now expose reusable target configurations via a templates map in their manifest, which projects reference from a target using template: "library/template". Properties from the template are loaded first and can be overridden by target-local settings.

Stdlib changes

  • Atomic.compare_exchange added.
  • Added array::contains_slice and array::index_of_slice.
  • String.index_of and rindex_of will now accept finding empty strings.
  • Add log::register_dynamic_category, for libraries that wish to define their own categories dynamically.

Fixes

  • $stringify would sometimes include parens.
  • Regression when destroying a BackedArenaAllocator in some cases #3332.
  • "a::b:c:d:e:f:0" was not parsed as a valid ipv6 string.
  • constdef vector with alias incorrectly lowered #3335.
  • Compiler asserts on concatenating a struct to an untypedlist #3326.
  • untypedlist was not detected as invalid in enum associated value type or as a pointer #3342.
  • Regression using non-posix libc.
  • Crash with an optional struct recursively defined with a function type. #3358
  • Denormal results were not handled correctly by String.to_double().
  • A float literal with an uppercase 'F' suffix would be a double instead of a float.
  • Json serialization would not correctly handle unicode and \v. #3353
  • Semantic checking was incorrect in the case of &a - &b where one or both are optional and the result isn't assigned.
  • Regression on MacOS, breaking stack trace.

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.

C3 0.8.1 Raiding the stdlib for bugs

"Bug fixes" is probably the least exciting thing you can lead with, but after 0.8.0 settled things in the language, it was natural to spend 0.8.1 hunting bugs.

Quite a few were found: over 100 fixes landed — most being corner cases, but a few are real, including a textbook Zip-Slip vulnerability in the zip extractor(!).

Outside of compiler development, there's also been significant progress on the specification here, which is necessary for C3 to be able to sign off on 1.0 in two years.

Language changes

Access reflected fields using a.$field and a.$field = b.

Previously, you could mutate and access fields on a struct by using $field.get(struct_value) and $field.set(struct_value, new_field_value). This is replaced by a much more streamlined struct_value.$field and struct_value.$field = new_field_value with the old variants deprecated:

struct Foo
{
    int i;
    bool b;
}

fn void test()
{
    var $field = $reflect(Foo.i);
    Foo f = { 45, true };
    // 0.8.0 prints 45
    io::printn($field.get(f)); 
    // 0.8.1 prints 45
    io::printn(f.$field);
    // 0.8.1 alternative syntax, also prints 45
    io::printn(f.$eval($reflect(Foo.i)));

    // 0.8.0 sets f.i to 44
    $field.set(f, 44); 
    // 0.8.1 sets f.i to 44
    f.$field = 44;
    // 0.8.1 alternative syntax
    f.$eval($reflect(Foo.i)) = 44;
}

Access to project path at compile time

env::PROJECT_PATH now returns the path to the project at compile time. This is mainly useful with $embed and similar compile time commands.

$vaarg[^1] supported

macro foo(...)
{
    // 0.8.0
    var x = $vaarg[$vaarg.len - 1];
    // 0.8.1 – with ^ support
    var x = $vaarg[^1];
}

Stdlib changes

Changes to API:

  • LinkedHashMap renamed OrderedMap, LinkedHashSet renamed OrderedSet. Old names are deprecated.
  • Enhanced path::ls functionality: it now supports wildcard search.
  • ini::parse and related take an error_line argument to identify the line with error.
  • Stricter JSON marshaling: it will return INVALID_NUMBER when encountering an inf or NaN for a float, and reject 1. literals.
  • @loop_over_ai would leak fds, deprecated and replaced by @loop_over_addresses.
  • spawn now allows binding I/O and using different settings per pipe.
  • io::write_all now retries on incomplete writes.
  • FixedThreadPool and ThreadPool are deprecated; they will be replaced by a new thread pool in a later 0.8.x version.

Additions to the stdlib

Concurrency & Threads

The BufferedChannel and UnbufferedChannel both gained non-blocking push/pop, using try_push and try_pop. In addition, UnboundedChannel was added. Such a channel will grow on demand, unlike BufferedChannel which has a fixed size.

This release also offers a preview of ThreadGroup for running tasks in parallel and collecting the results.

Platform support

libc::errno is now available on FreeBSD.

Compile time

The new values::expand macro turns strings containing expressions into values. This can be useful in some cases, as $reflect only creates statements.

Fixes

The 0.8.1 release contains over 100 fixes to stdlib, almost exclusively on the stdlib. The vast majority are stdlib corner cases.

The largest category is encoding and parsing: UTF-8/16/32 conversion edge cases (BOM handling, surrogate pairs, length detection), JSON's float precision and surrogate pair handling, varint signed-integer round-trips, base32/base64/codepage encoders that leaked memory on error, and PEM parsing on malformed input.

Memory management got a sweep. BackingArenaAllocator, DynamicArenaAllocator, OnStackAllocator, and the Vmem temp allocator each had at least one bug in a realloc, calloc, or destroy path. SortedMap had a possible array overflow. Deque.free didn't reset capacity, leaving the struct in a state that crashed on reuse. Wasm allocation could over-reserve unnecessarily.

Threading and synchronization got a check too: __atomic_compare_exchange had an incorrect implementation, lock_timeout on POSIX would sleep the full requested duration on every retry, the stack_size setting for thread creation was ignored on POSIX, and thread priority on Win32 was off by one. UnbufferedChannel could yield unpredictable values because its memory wasn't zeroed at creation.

Security: the Zip extraction code had a textbook Zip-Slip vulnerability — a malicious archive could write outside the target directory via .. components, absolute paths, or sibling-prefix paths in entry names. Fixed with up-front rejection of obviously-bad names plus a post-normalization containment check.

A handful of small correctness fixes touched types people use every day: DateTime.diff_years now handles leap years correctly, DString.replace handles empty needles and uninitialized strings without crashing, Formatter saturates instead of overflowing on absurd widths like %2147483648d, the URL parser stopped failing on user@host form and stopped silently dropping ports from bracketed IPv6 hosts, and process::run_capture_stdout no longer strips a character from output that doesn't end in \n.

Most of these are invisible if you've been using C3 successfully as they tighten the code paths nobody normally hits. But the stdlib now does the obviously right thing in considerably more corners than it did in 0.8.0.

Summarizing

To summarize: 0.8.1 is basically a stdlib release. Over 100 fixes — most are corner cases, a few are real (the Zip-Slip vulnerability in the zip extractor being the most notable). The user-visible additions worth a look: UnboundedChannel, try_push/try_pop on the existing channels, and a preview of ThreadGroup. The old FixedThreadPool and ThreadPool are deprecated; replacements are planned for later in 0.8.x.

So, same language as 0.8.0, just a bit sturdier library under it.

Thank yous

Again, 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.

PR contributors for this release

Stdlib: Alexandru Paniș, as2te, cmann1, Elusive239, Eugene Blikh, Fernando López Guevara, Johannes Müller, Kevin Hovsäter, Manu Linares, Maxine Bonnette and Zathyy

Compiler & toolchain: Akshat, Book-reader, Fernando López Guevara, Johannes Müller, Kevin Hovsäter, Manu Linares and Matthew Nagy

CI/Infrastructure: Fernando López Guevara, Manu Linares and plapinski

Change Log

Click for full change log

Changes / improvements

  • Add $$PROJECT_PATH, accessible through env::PROJECT_PATH.
  • Deprecate $field.get(a) and $field.set(a, b). Replaced by a.$field and a.$field = b.
  • Add a.$eval($field) as a variant of a.$field.
  • Add JSON pretty print.
  • $$atomic_store and $$atomic_load take an alignment parameter.
  • $vaarg[^1] is supported. #3276
  • Improve error message when a keyword is used as a block parameter. #3275
  • Correct tag method error messages from tagof/has_tagof to get_tag and has_tag
  • Don't resume parsing when implicit module names yield invalid names.

Stdlib changes

  • Add math::TAU / math::TWO_PI
  • Add values::expand to turn strings containing expressions into values.
  • Enhanced path::ls functionality, like searching for wildcard.
  • LinkedHashMap renamed OrderedMap, LinkedHashSet renamed OrderedSet. Old names are deprecated.
  • Added initial cpudetect on Linux / MacOS Aarch64.
  • Enable libc::errno for FreeBSD.
  • Checking filesize on Win32 now correctly reports errors. Getting the filesize now rejects directories.
  • ini::parse and related takes an error_line argument to identify the line with error.
  • JSON marshaling will return INVALID_NUMBER when encountering an inf or NaN for a float.
  • JSON decoding will reject 1. literals.
  • spawn now allows binding I/O and using different settings per pipe.
  • @loop_over_ai would leak fds, deprecated and replaced by @loop_over_addresses.
  • Correctly return error on native_fwrite and native_fread.
  • Prevent infinite spin on io::read_fully, File.load_buffer, File.load and File.save.
  • io::write_all now retries on incomplete writes.
  • GrowableBitSet.max_bit_set added.
  • Added UnboundedChannel.
  • BufferedChannel and UnbufferedChannel get non-blocking push/pop.
  • FixedThreadPool and ThreadPool deprecated.

Fixes

  • @volatile_store on arrays were sometimes incorrectly lowered.
  • NPOT vectors as associated variables were incorrectly lowered on load. #3228
  • .get_tag and .has_tag did not work properly for globals and locals.
  • Vectors stored in unions lowered incorrectly causing an assert #3234
  • Segmentation fault during library fetch when the "dependencies" key is missing in project.json. #3233
  • .tags would crash if no attribute with arguments were present.
  • Rect.merge_point would sometimes result in a point outside of the rect.
  • Possible array overflow in SortedMap.
  • Possible memory overwrite in BackingArenaAllocator on realloc.
  • Realloc could cause data corruption in DynamicArenaAllocator.
  • OnStackAllocator would not correctly clear memory on calloc.
  • Vmem temp allocator would not correctly free all vmem on destroy.
  • Wasm memory allocation could overallocate unnecessarily.
  • VirtualMemory contract off by one error.
  • CPU detect of leaf7 on x86 incorrect.
  • Fixed project benchmark target parsing. #3237
  • Incorrect type on UIntLE and UIntBE.
  • CVaList would behave different incorrectly for types larger than 8 bytes on some platforms.
  • UTF32 BOM detection was broken.
  • Sort from DString.less was inconsistent.
  • Fix io::skip using 'read' vs 'read_byte', causing an error.
  • Slice2d.slice incorrectly handled slices with x/y offset and 0/negative length together.
  • String.to_integer incorrectly accepted some invalid characters for hex.
  • Removed broken StringIterator.get.
  • Fix to refcount behaviour, preventing issue on release.
  • File.close should always invalidate the pointer on close, even on failures.
  • Overlong conversions to unicode for %c at boundaries.
  • Do not rely on implicit allocation for getcwd.
  • Skipping symlinks wasn't properly implemented for Win32.
  • Reverse indexing a value that overloads indexing would index an anonymous copy of the value.
  • Fix case where member.set would hit an assert.
  • Same type casts would not become rvalues.
  • Hex decoding would leak memory on failure.
  • Codepage.by_name would not use normalized name.
  • @return? bar! didn't work if the identifier matched a macro.
  • Copying compile time strings during compile time folding with strings containing 0 would sometimes get truncated. #3267
  • Pem parsing did not correctly handle an empty body, nor when the first line was too short.
  • Additional pem parsing bugs on malformed data handled.
  • Compiler would crash when getting the kind, qname, or alignment of an untypedlist.
  • untypedlist incorrectly had size property.
  • JSON handling of UTF16 surrogate pairs fixed.
  • base32, base64 and codepage would leak memory on encode/decode errors.
  • Indexing into a type with a $reflect value would sometimes cause a crash.
  • Using a faultdef hidden behind @if would cause a crash.
  • Taking the type of a macro method would cause a crash.
  • Cap array size to avoid overflow when making multidimensional arrays that are too large.
  • DynamicArenaAllocator would incorrectly handle some reuse cases.
  • __atomic_compare_exchange had an incorrect implementation.
  • channel::create_unbuffered would not correctly zero out memory, potentially yielding unpredictable results.
  • lock_timeout on Posix would sleep the entire sleep before retrying, and it would fail if it managed to sleep.
  • stack_size setting for threads was ignored on Posix.
  • Setting thread priority on Win32 was off by one.
  • Non-power-of-two-sized member of @bigendian bitstruct backed by char array wasn't working #3283.
  • Binary bitwise operations were not considered simple.
  • $expand was incorrectly made generic in generic modules. #3274
  • Mangle lambdas in macros without @ to ensure they work correctly on elf #3217.
  • DString.replace("", "X"); would crash.
  • DString.read_from_stream would not return the correct length when available was not supported by the stream.
  • @str_camelcase would yield same result as @str_pascalcase. #3287
  • conv::utf8to32 would not zero terminate when the zero would be at the end of the buffer.
  • char16_to_utf8_unsafe would not load low byte unaligned when required.
  • Not all invalid UTF8 was detected.
  • UTF16 length detection was incorrect for utf16 with surrogate pairs.
  • Initializing a variable which has the type of an optional struct using a const value would fail codegen. #3288
  • Parsing a malformed hex float would not correctly get reported.
  • Parsing an integer with trailing space would incorrectly be reported as an error.
  • String.escape used the incorrect default for stripping quotes.
  • mem::equals would not correctly compare slices with element size > 1.
  • AsciiCharset.contains incorrectly handled char > 127.
  • Reuse of recently freed DynamicArenaAllocator allocations failed.
  • Crash in codegen in some cases when RHS of a && or || was unreachable at lowering.
  • Visibility modifiers were incorrectly allowed on enum/constdef members.
  • Datetime format could not handle negative offsets with non-zero minutes.
  • NormalDist.random could occasionally return inf.
  • Url parser would fail on [email protected].
  • Url parser would drop the port on http://[::1]:8080.
  • Ipv6 classification - is_link_local etc, was incorrect
  • env::get/set_var for Win32 would appear to fail when succeeding.
  • env::get_var had a race condition on Win32.
  • process::run_capture_stdout would remove the last character, even when it wasn't \n.
  • Add missing __powisf2 to compiler_rt.
  • // would count newlines twice when parsing JSONC.
  • Path::for_posix(".a/..") was not parsed correctly.
  • SortedMap.clear and SortedMap.free would work incorrectly on map initialized with ONHEAP.
  • GrowableBitSet would yield the wrong length.
  • GrowableBitSet would not work correctly on backing types bigger than uint.
  • DString.replace would not work correctly in some cases.
  • ByteWriter.ensure_capacity did realloc unnecessarily when the data exactly matched capacity.
  • DString.equals used int rather than sz for len comparison.
  • DString.replace_char would crash on empty DString.
  • io::read_varint and io::write_varint: handling for signed integers was broken.
  • io::write_tiny_bytearray and io::write_short_bytearray could have incomplete writes.
  • Splatting a partially raw array into a macro would miscompile. #3302
  • Getting the tag for an enum parameter caused a crash. #3307
  • Json marshalling of floats would lose precision.
  • Crash when initializing a bitstruct from an untyped list.
  • Shifting a vector by a non-numeric type would cause a crash rather than a compiler error.
  • Recursive macros were not detected when going by way of a lambda.
  • Compile time concatenation with an empty slice was lacking checks, causing a compiler crash.
  • Fix zip slip vulnerability.
  • Fixed issues with Object.to_value.
  • DString.len was incorrectly marked @dynamic.
  • Qoi decoder wasn't correctly signaling all invalid data.
  • Casting a constant string to a float vector was buggy, causing a compiler crash.
  • Codepage detection could fail values after the last element.
  • Xml parsing could leak memory if root was preceeded by Pi nodes.
  • DateTime.diff_years would not handle leap years properly.
  • Deque.free would not reset the capacity, making it break if later reused.
  • Formatter would overflow in cases like %2147483648d.
  • Distributions would drop convergence control setting on recursion.
  • In some rare cases available() could leave the stream in an unexpected state.

Want To Dive Into C3?

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.

C3 0.8.0 The Core Language Is Settling

C3 is a programming language that evolves C – the same low-level model but with modern ergonomics. With 0.8.0, we're getting our first real glimpse of what C3 1.0 will look like: the core language design is now locking into its final shape.

Two changes are big enough to deserve their own posts. The compile time reflection system has been reworked into its final form — and in the process we got to kill off half the builtins: link. And we're making the move to signed by default, correcting what turned out to be a five-year mistake: link.

As with all 0.x.0 releases, we've tried to collect all the breaking changes into this single release, so the rest of the 0.8.x line can stay backwards compatible.

Aside from the reflection changes, these are the major changes:

Distinct types

Distinct types now defaults to be "structlike"

0.7.4 introduced the concept of @structlike which meant a distinct type could not implicitly convert from a literal. With 0.8.0 we flip the defaults: by default distinct types do not convert from literals, but with @constinit they do:

// 0.7.4
distinct Foo7 = int;
distinct Bar7 @structlike = int;

Foo7 f = 1;      // Ok
// Bar7 b = 1; ERROR!
Bar7 b = (Bar7)1; // Ok 

// 0.8.0
distinct Foo8 @constinit = int;
distinct Bar8 = int;

Foo8 f = 1;      // Ok
// Bar8 b = 1; ERROR!
Bar8 b = (Bar8)1; // Ok 

Often people were assuming "structlike" behaviour, cause accidental bugs. For this reason we flipped the defaults.

Enums and constdef

No more +/-

Doing addition on enums is no longer possible:

// 0.7.4
MyEnum foo = BAR;
MyEnum baz = foo + 1; // OK!

// 0.8.0
MyEnum foo = BAR;
// MyEnum baz = foo + 1; ERROR!
MyEnum baz = (MyEnum)(foo.ordinal + 1); // OK

Enhanced ++/--

Enums still support ++/-- and get implicit wrapping when overshooting the ends:

enum MyEnum
{
    ABC,
    DEF,
    GHI
}
fn void test()
{
    MyEnum e = ABC;
    e++; // e is now DEF
    e++; // e is now GHI
    e++; // e is now ABC through overflow wrapping
    e--; // e is now GHI through underflow wrapping
}

Removed inline enums

To simplify the language, inline enums were dropped.

// Ok in 0.7.x, error in 0.8.0
enum YourEnum : inline int
{
    TEST
}

int x = YourEnum.TEST; // Valid due to inline

Implicit conversion to ordinal when used as index

Enums will now implicitly convert to ordinals when used as index:

enum YourEnum8
{
    HELLO,
    WORLD
}

fn void test()
{
    int[2] x;
    x[YourEnum8.WORLD] = 123; // Same as x[1] = 123
}

.nameof has become .description

enum Foo
{
    TESTING,
    THE,
    NEW,
    DESCRIPTION
}

// 0.7.11
String s = TESTING.nameof;      // "TESTING"

// 0.8.0
String s = TESTING.description; // "TESTING"

Constdef now infers through unary negations

Unary negation would previously prevent constdef inference

constdef Abc
{
    ABC = 4,
    DEF = -4
}

Abc a = ABC; // Ok
Abc b = -ABC; // Ok in 0.8.0
Abc c = ~ABC; // Ok in 0.8.0

Operator overloading

Overloads for comparisons

@operator(<) is now added, so that a type which overloads both < and == can participate in all comparisons.

@operator(!=) removed

@operator(!=) was removed as it had limited usefulness.

Compile time enhancements

Shadow an a = ... parameter if it's not defined

Shadowing is now allowed:

macro @foo(a = ...)
{
    $if !$defined(a)
        var a = 123; // Previously a shadowing error
    $endif
    return a * 2;    
}

Use $eval as the name of a named parameter

foo(arg: 2);

// Possible since 0.8.0:
foo($eval("arg"): 2);

untypedlist as a new builtin type

During compile time, some compile time arrays get the type "untypedlist", containing a (possibly heterogenous) list of values. Previously it was possible to create it, but it wasn't possible to directly reference the type. This changes with 0.8.0:

// 0.7.11 - 'var' is the only possibility
var $foo = { 1, "hello", 3.14 };

// 0.8.0
untypedlist $foo2 = { 1, "hello", 3.14 };

The main use is to be able to test whether an expression is an untyped list or not. Regular variables cannot have this type, it's a compile time type only.

Added a tags property

An often requested functionality is getting all the tags on a type or member. This is now finally possible in 0.8.0:

struct Foo @tag("a", "hello") @tag("b", "test")
{
    int x @tag("c", 5);
}

int a_global @tag("d", 3.14);

String[] $tags_of_foo = Foo::tags; // { "a", "b" }
String[] $tags_of_x = $reflect(Foo.x).tags; // { "c" }
String[] $tags_of_a_global = $reflect(a_global).tags; // { "d" }

Also note that name for retrieving and testing for tags have changed to get_tag and has_tag.

Allow taking the type of an interface method

It's now possible to take the type of an interface method:

interface TestInterface
{
    fn void hello_world(String name);
}

$Typeof(TestInterface.hello_world) x;

$expand compile time function

The new $expand compile time function allows turning any string to code. For this reason it's inherently LSP/IDE hostile, but simplifies cases where otherwise $exec was the only option:

import std::io;

// This code prints "Hello"
$expand(`fn void hello() { io::printn("Hello"); }`);

fn void main()
{
    $expand(@sprintf("%s();", "hello"));
}

Use with care.

Removal of $xxxxof style builtins, updated type access

This has its own article: link

Contracts

@mustinit

This attribute enforces initialization of a type:

struct Foo @mustinit
{
    int a;
}

fn void test()
{
    // Foo f @noinit; ERROR, must be initialized
    Foo f2; // Ok, zero initialization is fine
}

Generics

Generic inference now looks through pointers

This did not work in 0.7.11, but will infer {int} in 0.8.0:

// "create_buffered" is a generic function
BufferedChannel{int}* c = channel::create_buffered(mem, 1);

Nested generics in generic functions/methods

An omission prevented this from working:

fn List{List{Type}} return_nested_list() <Type>
{
    List{List{Type}} l;
    l.init();
    return l;
}

Syntax changes

Removal of deprecated syntax:

  • Removed iXX and uXX suffixes.
  • Removed Enum.lookup.
  • Removed ? as suffix operator in the expression io::EOF?.

?? and ?: has new precedence and binds tighter than + and -

This change is tied to the removal of ? as a suffix operator. Previously there were essentially parser hacks to get the right precedence in various situations, such as foo() ?? io::EOF?!. The new precedence places ?? and ?: tighter than + -, but looser than | & ^.

With this tighter precedence, some things change meaning:

// Parses as (foo() ?? io::EOF~) + 3 in 0.8.0
int a = foo() ?? io::EOF~ + 3;

// Parses as (b ?: 4) + foo() in 0.8.0
int a = b ?: 4 + foo();

// Still parses as as foo() ?? (b | 3)
int a = foo() ?? b | 3;

$Typeof and $Typefrom instead of $typeof, $typefrom

This is a simple name change, to make it clearer that they can be in a type slot, as opposed to being a value.

alias Foo = int::typeid now works

macro typeid @get_type()
{
    return SOME_CONST ? int : double;
}

// 0.7.11
alias Foo = $Typefrom(@get_type());

// 0.8.0
alias Foo = @get_type();

Toolchain changes

docgen command for html documentation generation

This major addition to the C3 compiler now allows you to produce high quality documentation out of the box. This is also (finally!) ensuring that this site has up to date docs: link.

Support for Emscripten

The C3 compiler now has experimental support for Emscripten out of the box.

Reduced library dependency scanning

Only used libraries are now scanned for dependencies. Let's say you have two libraries in your /lib folder "a.c3l" and "b.c3l", "b.c3l" depends on "c.c3l" which isn't in the folder. However, your code only uses "a.c3l". Previously this would have been an error, signalling it could not find "c.c3l" despite it not being used which complicated cross platform compilation setups.

Windows installer for the C3 compiler

To simplify getting the C3 compiler up on Windows for beginners, there's now a Windows installer.

Stdlib changes

Changes to API:

  • BufferedChannel and UnbufferedChannel are now pointers
  • JSON api changes: parse -> load, parse_string -> parse. It now supports two flavors of JSON: JSON and JSONC
  • std::math name changes: PI_2 => HALF_PI, PI_4 => QUARTER_PI, DIV_PI => INV_PI etc, cosec => csc, cotan => cot, muladd => mad
  • std::time name changes: diff_hour => diff_hours. DateTime.set_date => DateTime.set, datetime::from_date_* => datetime::at_*
  • std::hash method name convention changes: updatec / update_char => update_byte.
  • std::string name changes: strip => strip_prefix, strip_end => strip_suffix.
  • Ordering of object::new_* arguments are now "allocator first".
  • InStream.set_cursor is renamed seek, and the old seek is removed.
  • The Path API now is split into PathPosix and PathWin, Path is implicitly castable to String and loses the str_view() method. Use path::tnew instead of path::temp for a temporary path.
  • PanicFn now takes an int for row.
  • ElasticArray renamed FixedList.
  • Updated ref::new argument order.

Additions to the stdlib

  • Add SortedMap based on skip lists.
  • Add OneShotChannel to std::thread::channel for single-send/single-receive thread synchronization.
  • Add std::collections::Deque.
  • Ini parser and encoder.
  • Mergesort added.
  • std::encoding::xml for XML parsing and serialization.

  • Make DString.append_repeat polymorphic adding append_string_repeat and append_char_repeat. DString.append_inline for optimized uses.

  • List and FixedList adds a remove_unordered_at.
  • channel::create_unbuffered and channel::create_buffered to create channels
  • String.compare_to and String.compare_to_ignore_case added.
  • RingBuffer now conforms to foreach and adds additional functions.
  • conv::detect_bom, converts utf16/utf32 from bytes with byteswaped / unaligned data.
  • Object.to_value was added to convert an object to a value.
  • Add multi part and extension support to Path.append.

Fixes

The 0.8.0 release contains around 50 fixes to stdlib but also addressing issues around vector lowering.

Summarizing

This is just the diff between 0.7.11 and 0.8.0. Compared against 0.7.0 the list would be huge — which might raise a worry: is the language not stabilizing?

But the opposite is true. The difference between 0.7.11 and 0.8.0 is actually fairly small. Most of the work from 0.7.0 to 0.7.11 went into refinements — closing gaps in semantics and functionality, and responding to concerns from real-world use. If you're looking at C3 from the outside, a small breaking-change diff this late in the 0.7 series is a good sign: the design is settling, and 1.0 is coming into view. The docgen work is a key piece of that puzzle, and it's now in place.

For 0.8.x we're looking forward to continuing to flesh out the stdlib, improving generics inference, and tackling the remaining tasks needed for 1.0.

Thank yous

Again, 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.

I'd like to especially thank Manu Linares who this month single-handedly implemented the Docgen in the compiler, and refreshed the website as well.

PR contributors for this release

Stdlib: Christian Reifberger, cmann1, corleypc, Darvisim, Fernando López Guevara, Manu Linares, Mathis Laroche, Nyr24, Peng He, Sander van den Bosch, surrealism21.

Compiler & toolchain: Dmitry Atamanov, Fernando López Guevara, Johannes Müller, Manu Linares, Tomás Lopes, Zack Puhl,

CI/Infrastructure: Fernando López Guevara, Manu Linares, LowByteFox

Change Log

Click for full change log

Changes / improvements

  • Removed "old-enums, old-slice-copy and old-compact-eq" feature flags.
  • Removed deprecated $evaltype.
  • Removed all deprecated (as of 0.7.11) types and functions from the stdlib.
  • Removed deprecated iXX and uXX suffixes.
  • Removed deprecated Enum.lookup.
  • Removed deprecated ? as suffix operator in the expression io::EOF?.
  • Removed deprecated module foo {Type} generic syntax.
  • Distinct types now defaults to be "structlike"
  • Removed @structlike attribute.
  • Removed deprecated @extern attribute.
  • : in contracts before description is now mandatory.
  • Removed deprecated Enum.associated (use Enum::members).
  • Removed deprecated Enum.elements (use Enum::len).
  • Removed deprecated foo_function.params (use $reflect(foo_function).params).
  • Removed deprecated $is_const.
  • Removed deprecated $assignable.
  • Enums now no longer directly support + and - – use ordinals instead.
  • For enums, using ++ and -- will step through enums with implicit wrap-around.
  • Rename isz -> sz.
  • Make $sizeof, $alignof and all similar functions return sz instead of usz.
  • Align literal types with C semantics.
  • Use value promotion instead of signedness promotion to int. So that small unsigned types promote to int, not uint.
  • Add a @mustinit attribute to enforce zero-initialization of a type. #3094
  • Improve error message when keyword is used instead of an expression. #3088
  • Add --warn-recursivecontracts.
  • Mutex.destroy and friends no longer return optionals.
  • Remove @operator(!=) overload.
  • Add @operator(<) overload, enabling type comparison overloads.
  • Generic inference can now look through pointer.
  • Enums now implicitly convert to their ordinal when used as indices.
  • Enums can no longer declare themselves inline.
  • Nested generics allowed inside generic functions/methods.
  • a = ... parameters may be shadowed if not defined.
  • $eval can now be used with named parameters, e.g. foo($eval("arg"): 2) #3090
  • Type properties are now accessed using :: and the "of" suffix, removed: int.sizeof -> int::size
  • Added $reflect with properties name, cname, qname, offset, alignment, size.
  • Added @kindof, @alignof and @sizeof macros.
  • Removed $nameof, $extnameof, $qnameof, $offsetof, $alignof, $kindof, $sizeof.
  • .nameof is changed to .description on fault and enum types.
  • Type property is_eq is renamed has_equals.
  • Type function tagof is renamed get_tag.
  • Add untypedlist as a usable type #2647.
  • ?? and ?: has new precedence and binds tighter than + and -
  • Added the tags property for types and $reflect.
  • Allow taking the type of an interface method.
  • Add $expand compile time function to convert strings to code.
  • Constdef now infers through unary negations.
  • Only used libraries are scanned for dependencies. #3144
  • $vaconst, $vaexpr and $vatype removed.
  • Improve error message on unsupported typeid runtime access at runtime. #3170
  • Added support for Emscripten.
  • Replace $vacount by $vaarg.len, replace $vasplat by ...$vaarg.
  • $vaarg behaves as $vaexpr.
  • Added docgen command to generate documentation.
  • Added jmpabs x86 CPU feature.
  • Implicit unsigned <-> signed integer conversions removed.
  • Added C3 Compiler setup installer for Windows
  • alias Foo = int::typeid now works.
  • $typeof => $Typeof, $typefrom => $Typefrom.

Stdlib changes

  • Add List.remove_unordered_at.
  • PanicFn now takes an int for row.
  • Add std::collections::Deque.
  • Add compare_to and compare_to_ignore_case to String. #3096
  • Add SortedMap based on skip lists.
  • Add OneShotChannel to std::thread::channel for single-send/single-receive thread synchronization.
  • BufferedChannel and UnbufferedChannel are now pointers, create using create_unbuffered and create_buffered
  • RingBuffer now conforms to foreach and adds additional functions.
  • Ini parser and encoder.
  • Updated ref::new argument order.
  • Support setting thread stack size.
  • Support setting thread priority.
  • Support syscall on RISCV.
  • Make DString.append_repeat polymorphic adding append_string_repeat and append_char_repeat.
  • Add DString.append_inline for optimized uses.
  • Ordering of object::new_* arguments are now "allocator first".
  • Add remove_unordered_at to FixedList.
  • Changed json to support two flavors of JSON: JSON and JSONC.
  • Changed json API: parse -> load, parse_string -> parse.
  • conv::detect_bom, convert utf16/utf32 from bytes with byteswap / unaligned data.
  • Mergesort added.
  • set_cursor is renamed seek, and the old seek is removed.
  • std::math name changes: PI_2 => HALF_PI, QUARTER_PI => PI_4, DIV_PI => INV_PI etc, cosec => csc, cotan => cot, muladd => mad
  • std::time name changes: diff_hour => diff_hours. DateTime.set_date => DateTime.set, datetime::from_date_* => datetime::at_*
  • std::hash method name convention changes: updatec / update_char => update_byte.
  • std::string name changes: strip => strip_prefix, strip_end => strip_suffix.
  • std::collections::object added Object.to_value to convert from an object to a value.
  • std::encoding::xml added for XML parsing and serialization.
  • Fix Path.append separator not honoring the specified environment.
  • Add multi part and extension support to Path.append.
  • The Path API now is split into PathPosix and PathWin, Path is implicitly castable to String and loses the str_view() method. Use path::tnew instead of path::temp for a temporary path.

Fixes

  • Slice comparison lowering would not work correctly in macros in some cases. #3095
  • Attributes @allow_deprecated, @constinit, @noalias, @nostrip, and @optional would erroneously accept parameters. #3098
  • Fix pipe handle leaks across concurrent process spawns #10067.
  • $$trap was incorrectly marked noreturn.
  • Recursive inclusion of contracts was not detected.
  • \r was not filtered when piping a source file from stdin.
  • SHA-3 and Keccak contexts are now explicitly @mustinit structures. #3110
  • UnbufferedChannel would deadlock on multiple producers.
  • Don't override sigaltstack when running with --sanitize=address. #3115
  • Binary search broken for some supported functions.
  • Fix bug casting (void*[<3>])x.
  • Compiler crash compiling a switch with a constant case range overlapping a constant case value. #3127
  • Incorrect handling of overaligned struct fields #3136
  • EnumSet with more than 128 entries was broken.
  • Handle underflow in zip.
  • Bugs in check for name suggestions on name mismatch.
  • Fix bug where only one ensure would not be inlined correctly. #3162
  • Incorrect error message when casting to non-existent enum.
  • Macro $Type = ... would not work correctly with $defined
  • Fix enum value handling in Object (std::collections::object) to conform with changes in enums.
  • Compiler assert in certain cases with ?? and void returns. #3168
  • Bug in compiler-rt for i128 shift.
  • LinkedBlockingQueue.push_timeout did not work correctly.
  • Splat into vaarg macro, where vaarg is not used #2782.
  • Comparison with floats had incorrect codegen, leading to incorrect results for NaN #3175.
  • Zeroing out simd vectors in a struct could in some cases lead to incorrect lowering #3179.
  • Incorrect lowering when returning a struct to an optional value on Win64 in some cases #3180.
  • Fix bug where a method is considered doubly generic if declared in a generic module for a generic type. #3176
  • Fix exp10 on platforms without exp10 as an LLVM builtin.
  • LLVM 23 compatibility: map Os/Oz to O2 pass pipeline, fix returnaddress intrinsic signature, add optsize/minsize function attributes.
  • Warning for ignored visibility modifiers was not emitted for macro methods #3071
  • while (String? x = foo()!) was accidentally allowed causing a lowering error.
  • Crash casting uint to bitstruct inside struct field assignment #3187
  • Vec2/Vec3 transform missed matrix translation.
  • Matrix rotation ignored matrix itself.
  • Fix BigInt shr, to_format, and others.
  • Fix ends in TDist.quantile, FDist.pdf, ChiSquaredDist.pdf
  • Fix to easing expo_in and bounce_inout.
  • deque with shrinking a zero sized list caused infinite loop.
  • Printing an enummap yielded the wrong character count.
  • Incorrect contract in FixedList allowed insert out of range.
  • Fix double-free in InterfaceList.
  • Object.set_at was incorrect.
  • Bitstruct with backing char[n] would occasionally be incorrectly stored.
  • fmuladd lowering crashes on a + -(b * c) with fastmath.
  • Constant folding -30 % -7 would incorrectly yield "2".
  • Parsing << in asm would not be correctly handled.
  • Incorrect lowering for float[<3>] when placed aligned in a struct.

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.

Let's kill off half the builtins in 0.8.0

Goodbye $sizeof, $alignof and all the rest

The problem

From the beginning, C3 mirrored C in having a sizeof builtin, which would take an expression and return the size. This was extended with additional similar builtins that would take an expression and return some reflection information about it.

To avoid colliding with anything else, it would get the -of suffix, and in line with C3's "$ means compile-time", they were prefixed with $.

At some point this became an actual challenge for naming things. $paramstructof doesn't quite roll off the tongue.

The solution: $reflect

So starting from 0.8.0, all of those are removed in favour of using the new $reflect builtin containing all attributes. For example, our $sizeof(a) becomes $reflect(a).size. In general, *of(a) becomes $reflect(a).*.

This might seem onerous, but first of all this allows us to store the reflection data and pass it to macros: some_macro($reflect(a)). But it's also trivial to construct macros that do the same thing. For example, @sizeof and @alignof are new macros that act as drop-in replacements for the previous builtins.

In 0.7.x you would use things like $defined($sizeof(x)) to test if something had a size defined. After the changes in 0.8.0, this becomes $defined($reflect(x).size) instead.

This change allows us to reduce the number of keywords and overall "language surface". Without explicit keywords, compile-time reflection actually becomes more flexible as well – it's easier to introduce additional introspection at some later time in the 0.8.x cycle if needed.

Stripping types of their properties

The problem

Up to 0.7.x, the corresponding type properties (related to the expression reflection previously discussed) were accessed using dot notation, e.g. int.sizeof. This reflected the corresponding builtin: int.sizeof <=> $sizeof(x), int.alignof <=> $alignof(x) and so on.

It would certainly have been nicer to use int.size, but properties would shadow method names:

struct Foo
{
    int len;
}

fn int Foo.size(self)
{
    return self.len;
}

fn void test()
{
    Foo f;
    Foo.size(f); // Valid C3    
}
So the .*of suffix was used exactly to avoid colliding with methods and field names.

However, as previously mentioned, naming with "-of" was increasingly a blocker. Could something be done?

The solution: :: to the rescue

The :: scope operator unequivocally separates module names from an identifier, and module names are always lower case. mymodule::submodule::Bar always means that everything left of the last :: is a module.

But what if we allowed Bar::baz?

This had no meaning in C3, because "Bar" could never be a module name.

In fact, int::size could not match anything. So what if we made :: the way to get type properties?

int::size now works, because the only thing that can appear to the right of a type name after :: is a type property.

// 0.7.11
const INT_SIZE = int.sizeof;
const INT_ALIGNMENT = int.alignof;
const INT_MAX = int.max;
// 0.8.0
const INT_SIZE = int::size;
const INT_ALIGNMENT = int::alignment;
const INT_MAX = int::max;

So the change both allows dropping "-of", and make it more consistent – like in the int.max case.

The big $vaarg reduction

The problem

The final big removal in C3 is the elimination of $vaexpr, $vacount, $vaconst, $vatype and $vasplat.

These builtins helped support macro "raw" vaargs. $vaexpr would paste in a vaarg as if it was declared like #foo, $vaconst like $foo and $vatype like $Foo.

Unfortunately, most of these were sparsely used.

$vasplat did splat the arguments, perfectly forwarding them into an initializer or another call.

However, $vasplat was conceived well before most of the ... splat functionality was in the compiler. In fact, it could be considered the precursor of most of the splat code.

The fact that it looked so very different from regular splats seemed unnecessary.

And $vacount? A keyword just to get the number of vaargs available?

The solution: one $vaarg to rule them all

While $vaarg did an adequate job to try to ensure that the expression was only evaluated once, the raw vaargs for macros largely behaved like lazy expression parameters ("#foo"). It was also always possible to recreate types and constants from $vaexpr as needed.

So if we just had $vaexpr, almost all functionality would still be available!

Since we already have ... for splat and .len to get length, we could derive everything from a single value. We renamed $vaexpr and called it $vaarg:

// 0.7.11
$for var $i = 0; $i < $vacount; $i += 2:
    self.set($vaarg[$i], $vaarg[$i + 1]);
$endfor

call_log(CRITICAL, category, fmt, $vasplat);

// 0.8.0
$for var $i = 0; $i < $vaarg.len; $i += 2:
    self.set($vaarg[$i], $vaarg[$i + 1]);
$endfor

call_log(CRITICAL, category, fmt, ...$vaarg);

Final thoughts

There is a natural contract-expand cycle to language design: first add features, then learn how they're used and how they can be simplified for the actual real use cases.

The builtins we're removing solved real problems, but they also added surface area: extra keywords, naming conventions, subtle interactions.

These simplifications were only possible now, after things like maturing the splat functionality, observing $vaarg usage-patterns and reaching the point where the old type property syntax prevented growth.

We hope you'll like them!

Unsigned sizes: a five year mistake

A quick note for readers who don't follow C3: it's a systems language in the C tradition. Specifics below are C3's, but the tradeoffs apply to any language that has to pick a type for sizes and lengths.

C3 is moving to signed by default, but why are we doing that? Isn't unsigned more correct for sizes at least? Let's try to answer that.

The bugs of unsigned

Since the early days, C3 has been using unsigned sizes. And while the name of the unsigned type changed over time – from "usize" to "usz" (after the unification with the uptrdiff type) – its position as the default has been unchallenged.

However, unsigned has known pitfalls, the most well known being:

for (uint x = 10; x >= 0; x--) // Infinite loop!
{ ... }

In fact, that bug is so easy to run into that C3 explicitly rejects x >= 0 for unsigned types outside of macros.

Another classic C bug is:

uint a = 0;
int b = -1;

if (a > b) { ... }

In C, both will promote to unsigned, turning b into a huge unsigned value, causing the comparison to fail. For this reason, C3 implemented safe unsigned/signed comparisons that wouldn't convert both sides and be safe regardless.

C, of course, allows implicit conversions between unsigned and signed. While this is a source of bugs, I felt that with some safety measures, it could mostly be kept.

It's easy to think that the bugs above are just unrelated quirks. The loop that never terminates, the broken comparison, the conversions that need to be fixed just-so... they all stem from one earlier decision: that unsigned should be the default for sizes. Most of this post is really about that decision.

A pertinent question

You might reasonably ask "but why not just require that signed/unsigned conversion is explicit?".

The reason, it turns out, lies with unsigned sizes.

If sizes are unsigned, like in C, C++, Rust and Zig – then it follows that anything involving indexing into data will need to either be all unsigned or require casts. With C's loose semantics, the problem is largely swept under the rug, but for Rust it meant that you'd regularly need to cast back and forth when dealing with sizes.

There are two approaches to casts: one is to liberally sprinkle them all over the codebase with the idea that "it's an explicit conversion, so it's obvious what happens". The other is to minimize casts, only using them to signal that something out of the ordinary is happening: "here be dragons".

The former is easier to define, but has the downside of essentially "silencing warnings". Let's say the code was originally written to cast an u16 to u32, but later the variable type changes from u16 to u64 and the cast is now actually silently truncating things. Here we have casts becoming a sort of "silence all warnings".

The main idea of "it's an explicit conversion" is also undermined by the practice of just putting in casts mechanically where the compiler says they're needed, rather than trying to actually examine every case.

On the other hand, minimizing casts is more challenging: rules are needed to correctly allow "safe" implicit casts, while requiring casts for what's unsafe.

C3 takes the second approach: casts should mean something, but why did it allow unsigned <-> signed? Isn't that unsafe?

It turns out that as long as you only use addition, subtraction and multiplication it's mostly quite safe if signed integers are 2s complement. And given that the conversions would need to happen often (remember: unsigned sizes!), the trade-off to make it implicit was natural.

The best laid plans

C3 has largely kept the current conversion semantics since 2021, and had been working reasonably well without triggering any serious undesirable semantics for 5 years, until an innocent question about "(foo + a) % 2" turned those assumptions on its head.

In order to remove footguns, C3 had picked "int + uint" to promote to "int" instead of unsigned. This made a lot more cases silently signed, which tended to be the correct thing in most cases. But what if we do (foo + a) % 2 and "foo" here happens to be over INT_MAX? Suddenly we get incomprehensible results! The right answer being (foo + a) % 2U instead.

This was an unacceptable problem. Not because it was hard to fix, but because it was so surprising. Almost everywhere else you could simply ignore if there was an underlying conversion to signed or not – it just worked. But / and %? Here's where the solution broke down. And because it "just worked" everywhere, it was fairly opaque what subexpression was signed or unsigned. The convenience made this minor issue into a big one.

The immediate reaction to this was to patch it: just issue an error on doing "unsigned / signed" and "unsigned % signed", but more issues were lurking in the shadows.

The tricky wrap

If you write a ring buffer, how do you make sure that calculating offsets are wrapping?

The naive solution is this:

index = (start + offset) % length;

This works as long as offset is positive. What about negative values? Here's a common simple solution:

index = ((start + offset) % length + length) % length;

Since offset is negative, we can assume signed numbers, so barring extremely large offsets (causing signed overflow) this will work.

(Note: if % had yielded the modulo rather than the remainder, the naive solution would have worked)

Now, remember how we started with unsigned sizes? Unsigned first will likely lead us to using all unsigned, leading to code looking like this:

index = ((start - offset_back) % length + length) % length;

Which is completely wrong – but also hard to detect. It will sometimes wrap correctly, but mostly not.

The correct code for unsigned needs to look something like this:

index = (start + length - (offset_back % length)) % length;

Regardless of what rules we apply to unsigned and unsigned-signed conversions, there is simply no way for the compiler to let us know that the first "offset_back" example is broken for unsigned.

So let's back up a bit.

The unsigned size

It seems hard to solve the problem with unsigned, so is there some faulty assumption we're making?

Let's look back in time: C initially was about using signed integers being designed around the int type. This all changed when the type of sizeof was standardized to the unsigned size_t.

This single change single-handedly introduced unsigned arithmetics as a common thing in C code. Finding this new shiny thing, people started to use unsigned to encode "this value can't be negative" and talk about how using unsigned helped since it allowed them to express larger sums.

That didn't mean it was without problems. In fact the problems were so significant that in the 90s, Java decided to drop unsigned types entirely in its design. Java's reaction was perhaps a little extreme, but it did achieve the goal of making a large set of common bugs – related to unsigned – just go away.

Go should give us pause: it's a low-level language, created as a reaction to problems in C++, by people who knew exactly what unsigned sizes cost - and they picked signed sizes.

With any bounded integers, problems arise when we close in on the boundaries. For a 32-bit signed int that is approximately at plus and minus 2 billion, for an unsigned 32-bit integer, it's at 0 and approximately 4 billion. The "unsafe" boundaries for unsigned lie so much closer than for signed integers – there is simply no contest.

This is exactly why we see problems for things like in the case with %.

But what about the range? While it's true that you get twice the range, surprisingly often the code in the range above signed-int max is quite bug-ridden. Any code doing something like (2U * index) / 2U in this range will have quite the surprise coming. But it's worse than that: overflow for signed values generally produces an invalid, negative number – but unsigned overflow often produces a quite plausible number, just the wrong one. Not to mention that on modern 64 bit machines, you'll run out of memory before you can use a full signed 64 bit integer.

Ok, but isn't it valuable to be in the right range by design? The answer seems to be no judging from work on verification frameworks, as unsigned only encodes modulo behaviour and actual ranges. It might be argued that you can make unsigned overflow an error (this is indeed what Rust does), but that removes useful properties of unsigned arithmetics: (a + b) - c is equal to a + (b - c) if unsigned arithmetics wrap, but is not the same if it doesn't. This is a trap in itself.

So we have unsigned quite frequently used, more or less by historical accident. It's error prone and silently hides errors. So maybe the solution isn't trying to make it more ergonomic?

Signed first

As you might have anticipated, C3 has come to adopt signed sizes for types and lengths. Since unsigned now becomes more rare, we don't need any implicit conversions between unsigned and signed. Comparisons between unsigned and signed? – also gone.

When doing this change I started removing unrelated uint and ulong usages as well, and I discovered code that seemed suspicious or just plain wrong. Also, code just got plain simpler with just int and signed sizes everywhere. This is where I realized I had been internalizing the cost of using unsigned: after a while working in C or C++, you get the habit of looking for possible problems due to unsigned, and using patterns that are less obvious, but are sure to work for both unsigned and signed variables.

I'm a bit embarrassed about how long it took for me to change this, and it's a testament to how deeply ingrained the habit was. I just assumed unsigned sizes was the way to go, and that the problem was simply to improve ergonomics and eliminate as many pitfalls as possible. This despite both Go and Java showing the way with signed sizes.

But even after deciding on the change, converting from unsigned to signed felt awkward and wrong at first, as if I was doing something forbidden – that's how far gone I was. But seeing how each change both made the code easier to reason about and more correct, I couldn't deny the evidence.

Some notes on the changes in C3

This change was discussed in the C3 discord before it was implemented and got the affectionate name "iszmageddon", this is in reference to the isz type (corresponding roughly to ssize_t) becoming the default type of sizes.

In order to more clearly promote the signed size, it was renamed just "sz", giving 0.8.0 the asymmetric pair sz/usz. This makes it easy to remember which one is preferred. Consequently the change was renamed "szmageddon".

Originally the implicit conversion between signed <-> unsigned was mainly left intact, but it was later completely dropped.


Discuss this article on Hacker News.

C3 0.7.11 - The last v0.7

With 0.7.11 we've reached the end of the 0.7 era. It's been a really good year for C3, improving on rough edges and expanding the stdlib.

This release is headlined by an updated matrix library. Aside from that, it’s a refinement release, bringing numerous fixes but no major changes.

Language changes

constdef inference through binary operations

constdef is often used to define masks:

constdef Foo
{
    AUDIO = 0b01,
    VIDEO = 0b10,
    /* ... */
}

In 0.7.10 inference worked through assignment but not expressions:

Foo f = AUDIO; // Ok
f = Foo.AUDIO | Foo.VIDEO; // Ok
// f = AUDIO | VIDEO - this is an error

From 0.7.11 onwards, the inference works:

f = Foo.AUDIO | Foo.VIDEO; // Ok
f = AUDIO | VIDEO; // Also ok in 0.7.11

Updated @weak

@weak, which changes linkage, now also supports having multiple definitions of the same declaration in the same source code, allowing the non-"weak" definition to win. This allows things like changing code from this:

fn void foo() @if(env::POSIX)
{
    io::printn("Works!");
}

fn void foo() @if(!env::POSIX)
{
    abort("Unsupported foo()");
}
To this:
fn void foo() @if(env::POSIX)
{
    io::printn("Works!");
}

fn void foo() @weak
{
    abort("Unsupported foo()");
}

Warning on $$builtin use

Builtins (functions prefixed with $$, such as $$unreachable) are intended to be accessed through standard library macros, not used directly. They are considered internal and may change without warning.

This wasn’t previously clear, and some code ended up using them directly. The compiler will now issue a warning when such builtins are used outside the standard library.

Zero element enums forbidden

Before 0.7.11, the language allowed empty enums. In practice, they were not fully supported and could lead to incorrect behavior.

Given their limited usefulness and the inability to define a valid zero value, empty enums have been removed from the language.

Misc improvements

C3 now also detects large temporaries when creating slices on the stack.

Standard library

Updated Matrix library

The big change is the updated Matrix library. The new matrix type is column major, aligning it with most graphics and math libraries. It has also undergone quite an overhaul, with methods and functions updated and fixed. The default aliases are now based on floats rather than doubles, which fits with common usage.

The predefined aliases are:

  • Quat - quaternion
  • Mat2 - 2x2 matrix
  • Mat3 - 3x3 matrix
  • Mat4 - 4x4 matrix
  • Vec2 - 2d vector
  • Vec3 - 3d vector
  • Vec4 - 4d vector
  • Rect - 2d rectangle

The matrix perspective and ortho, project and unproject functions are now right-handed [0,1].

std::mem improvements

  • std::mem::allocator is deprecated and split into std::core::mem::allocators containing allocators and std::core::mem::alloc for various allocation methods. This means replacing most allocator::* calls with alloc::*, e.g. allocator::calloc becomes alloc::calloc.
  • @unaligned_load and @unaligned_store are deprecated in favour of mem::load and mem::store, which also supports unaligned volatile load/store operations.

std::encoding improvements

  • Serialization to and from structs using JSON is now available.
  • Functions for gzip compression and decompression added.
  • Support for AES encrypted Zip files
  • Base32/Base64/Hex/Codepage encoding deprecated encode_buffer/decode_buffer, replacing them with encode_into and decode_into.

Crypto / hashes

  • Keccak, SHA3, Shake, CShake, kmac, Turboshake, Tuplehash and Parallelhash were added.
  • The remaining Xoshiro and xoroshiro PRNG variants were added.
  • Added Argon2 hash.
  • entropy module for generating cryptographically secure random bytes.
  • random::seeder no longer uses temp memory.

Date / Time

  • DateTime and DateTimeTz are now Printable and can be used directly with printf
  • DateTime now has a to_format method.

Misc improvements

  • The backtrace has been cleaned up on Linux.
  • ZString now has a hash method.
  • Simple member-wise struct comparison using member_eq.
  • always_assert macro, which asserts even in unsafe mode.
  • file::last_modified was added.
  • SubProcess was renamed Process and refreshed with new, more streamlined, functions.
  • Use methods short_name() and @short_name() to get the unqualified fault name, e.g. io::EOF becomes EOF.

Toolchain

Removal of support for LLVM 17/18 and fixes for LLVM 22

On most platforms, the C3 compiler is linked with custom-built LLVM libraries, which reduces the need to support older versions of LLVM.

0.7.10 was incompatible with LLVM 22, which is fixed in this version.

Fully static builds of C3C for Linux

With 0.7.11, MUSL-based builds of the compiler are available on Linux.

Unified SDK fetching with Android support

0.7.10 brought automatic download of the MSVC SDK without needing external scripts. In 0.7.11, this is extended to support Android, with the goal of bringing in more targets, such as the MacOS SDK for effortless cross-compilation.

Bug fixes

0.7.11 brings many major and minor bug fixes, see the complete release notes for details.

Looking Forward

Next up will be 0.8.0. As usual, this means there will be breaking changes. The most obvious target is all the deprecated functionality, from enum Foo : const int to the old matrix library.

However, the big upcoming change is the so-called "Szmageddon": C3 will change from unsigned sizes (usz) to signed sizes by default. This will also affect type promotion rules and literal types.

Community and Contributions

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.

PR contributors for this release:

Stdlib: Alexandru Paniș, Book-reader, Bram Windey, Dave Akers, Disheng Su, Flanderzz, Konimarti, Manu Linares, LowByteFox, Siladi, Skunky, Technical Fowl, Zack Puhl

Compiler & toolchain: Dmitry Atamanov, Lucas Alves, Manu Linares, Nmurrell07, Rodrigo Camacho

CI/Infrastructure: Manu Linares, Mehdi Chinoune, Sisyphus1813, Zack Puhl

Change Log

Click for full change log

Changes / improvements

  • Removed support for LLVM 17, 18.
  • Detect large temporaries when creating slices on the stack #2665
  • Search for the linker in PATH; use the builtin linker if CC missing. #2906
  • constdef inference through binary expressions: Foo f = Foo.AUDIO | Foo.VIDEO can be written Foo f = AUDIO | VIDEO;
  • Fix for LLVM 22+ compatibility #2987
  • @weaklink for just affecting linkage.
  • Add a fully static build of c3c for Linux. #2949
  • @weak now allows direct overriding of @weak definitions with a real definition.
  • Unified SDK fetching under c3c fetch-sdk <target> (windows, android) and added support for automatic Android NDK (r29) download. Better progress bar. #3019
  • Improved Linux backtrace readability by stripping internal panic and runtime startup frames. #3008
  • Added repetition compression for deep recursive stacks in backtraces. #3008
  • Added new builtins: $$acos, $$asin, $$atan, $$cosh, $$exp10, $$sinh, $$tan and $$tanh.
  • Added the rest of the xoshiro and xoroshiro PRNG variants. #3027
  • Improve error when using keyword as identifier #3066
  • Warn when using $$builtin functions outside of the stdlib #3065
  • Zero element enums now disallowed.

Stdlib changes

  • Add contract on any_to_enum_ordinal and any_to_int to improve error when passed an empty any. #2977
  • Add hash method for ZStrings. #2982
  • Added json serialization from structs.
  • Add keccak and Keccak-based hash functions: sha3, shake, cshake, kmac, turboshake, tuplehash, and parallelhash. #2728
  • Added fault.short_name and fault.@short_name to get just the fault name for both run and compile time. #3002
  • Compiler runtime functions extracted outside of std.
  • Add the GZIP file format (RFC 1952).
  • Add file::last_modified.
  • Make DateTime and DateTimeTz Printable.
  • Add to_format functionality for DateTime.
  • SubProcess/process::create/process::execute_stdout_to_buffer deprecated, replaced by Process/process:spawn/process::run_capture_stdout.
  • Add support for AES-encrypted Zip files (AE-1 and AE-2 formats).
  • Add Argon2 memory-hard hashing with associated tests. #2773
  • Matrix type is now column major.
  • Fix matrix perspective and ortho, project and unproject to be RH [0,1]
  • Add vec3 methods: rejection, project, implement unproject.
  • Add vector function cubic_hermite
  • Deprecated sq_magnitude, barycenter, towards, ortho_normalize, clamp_mag, use length_sq, barycentric, move_towards, orthonormalize, clamp_length instead.
  • Add Quaternion conversion functions to from Euler angles and axis+angle.
  • math::deg_to_rad and math::rad_to_deg respects the underlying type, returning float on a float argument.
  • Added float.to_radians and float.to_degrees and the same for double.
  • Added Quat, Mat2, Mat3 and Mat4, Vec2, Vec3, Vec4 aliases.
  • Added is_normalized to Quaternion and floating point vectors.
  • Added quaternion::from_rotation and quaternion::from_normalized_rotation
  • Added Rect type.
  • Added matrix::frustum.
  • Added math::@abs for compile time abs.
  • Make Errno a constdef containing all definitions. Deprecated libc::errno constants.
  • random::seeder no longer uses temp memory.
  • Add simple member-wise struct comparison with member_eq. #2801
  • std::core::mem::allocator deprecated and split into std::core::mem::allocators containing allocators and std::core::mem::alloc for various allocation methods.
  • Add always_assert builtin macro.
  • Add an entropy module to generate cryptographically-secure random bytes. #3022
  • Add a builtin TIMEOUT fault definition. #3022
  • Base32, Base64, Hex and Codepage encoding deprecates encode_buffer and decode_buffer. Those are replaced by encode_into and decode_into with dst being the first argument. #3055
  • hex::encode_bytes and hex::decode_bytes are deprecated in favour of hex::encode_bytes_into and hex::decode_bytes_into which has dst the first argument. #3055
  • Deprecation of @unaligned_load and @unaligned_store. Use mem::load and mem::store instead.

Fixes

  • @deprecated in function contracts would be processed twice, causing a compilation error despite being correct.
  • Name conflict with auto-imported std::core, but it should have lower priority #2902
  • Regression: missing generic nesting check on non-types.
  • Improved stringify.
  • PollSubscribe was incorrectly an int instead of ushort. #2997
  • SubProcessOptions.search_user_path did nothing on non-windows systems despite comment saying it should #2845
  • AES implementation fixed to be constant time #2806
  • Object would not properly compile on 32-bit Linux.
  • read_varint and write_varint did not work properly for ulong and wider.
  • io::EOF.nameof would yield just EOF whereas resolving it at runtime would (correctly) yield io::EOF.
  • $stringify would incorrectly capture lambdas. #2986
  • Regression: String was not implicitly @constinit #2983
  • Compiler does not propagate @noreturn through macros using short declaration syntax #3011
  • Debug info emitted on -Os #3015
  • @assert_leak() would not work properly with --safe=no #3012.
  • Duplicate symbols when building executables on Termux. #2984
  • double[<*>].max and .min were broken.
  • Incorrect codegen, crashing the compiler, when passing a { .xy = 1 } constant initializer vector to a function taking a vector, hitting vec->array conversion. #3035
  • Folding an anon struct member at compile time would crash #3034.
  • Crash in sema_compare_weak_decl when replacing a function declaration from a .c3i file in some cases #3031
  • Issue with 'inline' keyword on enum and constdef #3032.
  • When checking aliases alias FOO = _BAR the compiler would incorrectly would say that _BAR wasn't a constant.
  • Wasm32 builds crash on startup (unreachable!) due to atexit signature mismatch #3040
  • @nodiscard, @maydiscard and @noreturn weren't properly handled for function type declarations.
  • $defined with body expansion would not correctly check if parameters were the right type.
  • mask_from_int would miscompile on some platforms.
  • Overaligning structs while using @packed would cause incorrect lowering #3000
  • Splatting a literal into a typed vaarg, e.g. test(...(int[2]){ 88, 99 }, a: 123) could cause the compiler to crash.
  • &some_global[0] was incorrectly considered a global constant when some_global was a slice.
  • Taking the type of a macro identifier would give the wrong error.
  • Taking the type of a $$builtin function would crash the compiler.
  • Wrong error message when trying to take the address of a $$builtin function.
  • Accessing a (non-existing) property on a type-call would crash the compiler.
  • Crash instead of error when having two vaargs and the last one is an untyped vaarg.
  • Detect recursive declaration int[type()] type.
  • Compiler would not propagate error when $$str_find or $$str_hash arguments were invalid, causing a crash.
  • Error on wrong expression when the slice range start is out of range.
  • void{} would be looked up as generic in some cases and cause a crash.
  • Inferring generic parameters recursively would fail to construct a valid source location and crash.
  • Comparing an array of function pointers with any other type could crash rather than being an error.
  • Crashing on codegen if an internal fault in if-catch is guaranteed to bypass the conditional.
  • In $foreach in some cases the elements was an untyped variable which would cause a crash.
  • Creating a global slice would be runtime checked for null in some cases.
  • @ensure and @require could contain rethrows, which then would crash the compiler.
  • Crash when using $defined($Type) and $Type is a typeid.
  • Assigning to a subscripted const like {1, 2}[n] = 33 wasn't marked as an error and resulted in a crash.
  • Codegen for the case when an assert always panics would cause a crash.
  • Lambda check might run against a missing type definition if the function type alias was invalid.
  • Missing check when doing $foo++ would crash the compiler if the variable wasn't initialized.
  • Incorrect handling of attribute operator symbols could crash the compiler instead of producing an error.
  • Crash instead of error when the first method parameter is a vaarg.
  • Fixes to UnalignedRef.
  • Codegen would not pop debug location for a never-entered for loop, crashing LLVM lowering.
  • Double negating a vector would cause a crash in lowering.
  • Combining operator overload on a variadic method would cause a crash rather than emitting an error in some cases.
  • Lambdas as default arguments were tagged with the wrong module, leading to linking issues.
  • An initializer list with an optional field was incorrectly considered constant.
  • Fix in ringbuffer for the case of popping at position 0.

Want To Dive Into C3?

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 or Hacker News.

C3 0.7.10 - Constdef finally takes shape

After the big enhancement for generics in 0.7.9, coupled with the large number of bug fixes, 0.7.10 is naturally a more modest improvement.

A big change with the 0.7.10 release is that it's built with custom LLVM builds. This allows us to compile without unnecessary dependencies (the c3c binaries infamously depended on libxml2.so.2 due to the LLVM.org precompiled static libraries needing it).

The major change in 0.7.10 is resolving the fate of "const enums", but it also makes some other quality-of-life improvements to the language.

Constdef

Very early on C3 made the change from C-style "enum is a number" to a strictly ordinal backed enum. What this means, is that there are no gaps in C3 enums.

If we look at this enum in C, it's perfectly representable in C3:

enum Foo
{
    ABC,
    DEF,
    GHI,
    JKL
};

The corresponding C3 enum would simply be:

enum Foo
{
    ABC,
    DEF,
    GHI,
    JKL
}

However, if we had the following C enum, C3 regular enums would not be able to match it:

enum Foo
{
    ABC,
    DEF = 3,
    GHI,
    JKL
};

Because now the enum would have a gap, missing the ordinals 1 and 2.

The 1:1 mapping between C3 enums and integers allows C3 enums to support name lookups and associated values without overhead:

enum Greet : int (String fmt, String country)
{
    HELLO { "Hello %s", "USA" },
    NIHAO { "%s, 你好", "Taiwan"},
    HEJ { "Hej %s", "Sweden" }
}

fn void greet(Greet g, String name)
{
    io::printn("Using %s for greeting in %s.", g.nameof, g.country);
    io::printfn(g.fmt, name);
}

fn void test()
{
    // Prints
    // "Using HEJ for greeting in Sweden."
    // "Hej Sven"    
    greet(HEJ, "Sven");
    // Prints
    // "Using NIHAO for greeting in Taiwan"
    // "小龍, 妳好"
    greet(NIHAO, "小龍");
}

In this example, the lookup of .nameof, .country and .fmt is just indexing into an array.

If this had been C, we'd instead have to maintain such an array manually, alternatively solve it with a switch statement:

// C equivalent of .country
const char *get_country(enum Greet g)
{
    switch (g) {
        case GREET_HELLO: return "USA";
        case GREET_NIHAO: return "Taiwan";
        case GREET_HEJ: return "Sweden";
        default: return "Unknown country"
    }
}

This need for manual maintenance often leads to bugs, often in the worst possible situation - like when printing to an error log.

So the problems C3's enum is addressing are important to fix, but there is a problem – what about the situation when you need enums with gaps?

Enums as groups of constants

C's enums conflate two things: (1) a closed set of values mapping 1:1 to an underlying value (2) a grouped set of constants with a distinct type.

A classic variant of (2) in C is defining masks:

typedef enum 
{
    MASK_ABC = 1 << 0,
    MASK_DEF = 1 << 1,
    MASK_GHI = 1 << 2
} Mask;    

Mask start = MASK_ABC | MASK_GHI;

In this case there is no intention to go back and forth from value to enum, since values do not map to a single value. Our MASK_ABC | MASK_GHI has the value 5, which doesn't match any of the defined enum values.

The C usage is fine, but enums defined in this way can certainly not have an array or switch lookup to find the name, because there is not even a single name to match on!

Other usages are matching on binary protocols, where the underlying value of each enum value is language-independent. Furthermore, long-term some values may get deprecated and later completely removed.

This type of enum is not a closed set of values, but rather an open set of related constants.

In C, these two are conflated, but that approach is difficult in C3.

Emulating C enums

The first approach tried in C3 was to create a distinct type with a custom submodule:

// C version
typedef enum {
    FLAG_VSYNC_HINT         = 0x00000040, 
    FLAG_FULLSCREEN_MODE    = 0x00000002, 
    FLAG_WINDOW_RESIZABLE   = 0x00000004,
    ...
} ConfigFlags;

// Usage
ConfigFlags flags = FLAG_VSYNC_HINT | FLAG_FULLSCREEN_MODE;
// Early C3 version
module raylib;
...
typedef ConfigFlags = int;
// Submodule contains the constants
module raylib::config_flags;
const VSYNC_HINT         = 0x00000040; 
const FULLSCREEN_MODE    = 0x00000002; 
const WINDOW_RESIZABLE   = 0x00000004;

module my_game;
import raylib;
// Usage
ConfigFlags flags = config_flags::VSYNC_HINT 
                    | config_flags::FULLSCREEN_MODE;

As we can see, usage is very similar, and we get the same distinct type as in C, but there's a significant amount of song and dance to create the config flags.

There was a lot of experimentation in early versions of 0.7.x to allow regular enums to "convert" to integers, this culminated in this functionality:

// C3 inline enum values
module raylib;

typedef ConfigFlagVal = int;
enum ConfigFlags : int(inline ConfigFlagVal val)
{
    VSYNC_HINT         = 0x00000040, 
    FULLSCREEN_MODE    = 0x00000002, 
    WINDOW_RESIZABLE   = 0x00000004,
}

module my_game;
import raylib;
// Usage
ConfigFlagVal flags = ConfigFlags.VSYNC_HINT 
                      | ConfigFlags.FULLSCREEN_MODE;
// Implicitly what happens is
// ConfigFlagVal flags = ConfigFlags.VSYNC_HINT.val
//                       | ConfigFlags.FULLSCREEN_MODE.val;

However, this was deemed way to fiddly and advanced to do, which eventually led to capitulation and the const enums were introduced in 0.7.4:

// C3 const enums
module raylib;

// "const" creates a const enum
enum ConfigFlags : const int
{
    VSYNC_HINT         = 0x00000040, 
    FULLSCREEN_MODE    = 0x00000002, 
    WINDOW_RESIZABLE   = 0x00000004,
}

module my_game;
import raylib;
// Usage
ConfigFlags flags = ConfigFlags.VSYNC_HINT 
                    | ConfigFlags.FULLSCREEN_MODE;

Now we're basically having C enums. But there is something not so nice with this:

enum Foo : const int
{
    ABC,
    DEF
}
enum Bar : int
{
    ABC,
    DEF
}

fn void test()
{
    io::printn(Foo.DEF); // Prints 1
    io::printn(Bar.DEF); // Prints DEF
}

Enter "constdef"

The C enums were quite different, but shared basically all visual similarities. So it was decided to rename these, there were alternatives such as these:

const enum Foo
{
    ABC,
    DEF
}
cenum Foo
{
    ABC,
    DEF
}
enumc Foo
{
    ABC,
    DEF
}

During this discussion, it was revealed that a lot of people just defaulted to enum Foo : const int. Even just calling them const enum got people to think they were essentially the same as the regular enums. Some people even expressed the opinion that the const enums should be the default enums.

This showed how the problem had become one of communication: by sharing a similar name, people assumed the same functionality. It just seemed that "const enums" were just like regular enums but "better", because they appeared to give more options, even though as we saw above, const enums weren't able to provide any of the useful features of the regular C3 enums.

A major break with the old syntax was needed. So rather than the conservative "const enum" or "cenum", the name constdef was chosen. Since faultdef, typedef and attrdef was already established syntax, that name was in line with other C3 keywords.

So the 0.7.10 version of the example becomes

// C3 constdef
module raylib;

constdef ConfigFlags : int
{
    VSYNC_HINT         = 0x00000040, 
    FULLSCREEN_MODE    = 0x00000002, 
    WINDOW_RESIZABLE   = 0x00000004,
}

module my_game;
import raylib;
// Usage
ConfigFlags flags = ConfigFlags.VSYNC_HINT 
                    | ConfigFlags.FULLSCREEN_MODE;

So semantically we're using the enum Foo : const int of 0.7.4, but with a name that clearly indicates its capabilities. In essence it is not very different from the approach of typedef + const that we started with, but it's much cleaner.

The change in name makes the trade-off obvious: pick enum and you can't change the underlying value, pick constdef and you don't get runtime name reflection.

constdef goes further than C enums though. It may be any value, not just an integer. So this is just as valid:

constdef GreetingFmt : String
{
    HELLO = "Hello %s",
    NIHAO = "%s 你好",
    HEJ = "HEJ %s",
}

This would be just the same as defining the constants:

typedef GreetingFmt = String;
const GreetingFmt HELLO = "Hello %s";
const GreetingFmt NIHAO = "%s 你好";
const GreetingFmt HEJ = "HEJ %s";

So this extends the C idea of "enums as a distinct set of constants" from just integers to any type.

To further make enums and constdefs distinct, the syntax from the inline experiment have been reverted, and to declare associated values for enums {} is now used:

// Pre 0.7.10
enum Foo : (int a)
{
    ABC = 2,
    BCD = 3,
}
enum Bar : (int a, String b)
{
    TEST1 = { 1, "a" },
    TEST2 = { 75, "foo" }
}
// 0.7.10
enum Foo : (int a)
{
    ABC { 2 },
    BCD { 3 },
}
enum Bar : (int a, String b)
{
    TEST1 { 1, "a" },
    TEST2 { 75, "foo" }
}

Compatibility with 0.7.9 and earlier

As per usual, the "const enum" syntax will still work until 0.8.0. Likewise, the old style of declaring associated values for regular enums will keep working. By default, a deprecation notice will be shown, but this can be suppressed by using --warn-deprecation=no as a command line option.

Typedef literal conversion changes

In 0.7.9 and before, distinct types defined with typedef would implicitly convert from any literal or constant value. To avoid this behaviour you needed to add @structlike. However, it's been established that this is the wrong default. With 0.7.10 the default is swapped up: use @constinit to allow implicit casts from constants and by default it's not supported.

However, for backwards compatibility the old behaviour will only yield a deprecation notice.

// 0.7.9 behaviour
typedef MyNumber = int;
typedef Temperature @structlike = int;

MyNumber n = 0;
// Temperature t = 0; Error: needs explicit conversion
Temperature t = (Temperature)0; 

// 0.7.10
typedef MyNumber @constinit = int;
typedef Temperature = int;

MyNumber n = 0;
// Temperature t = 0; Error: deprecated
Temperature t = (Temperature)0;

Method resolution and $defined

A problem has been using $defined with methods, since methods are associated with their underlying types fairly late. For this reason 0.7.9 tightened the constraints as to when it was possible to test a method.

Here's a problematic example where the dependency is circular:

struct Foo { int a; }

fn void Foo.test(&self) @if($defined(Foo.test2))
{}
fn void Foo.test2(&self) @if($defined(Foo.test))
{}

While it's important to detect these kinds of circular dependencies, 0.7.9 would also end up disallowing well-ordered used of $defined with methods.

This changes in 0.7.10. Rather than checking if the method resolution is complete before $defined is invoked, the compiler will "tag" the parent type when its methods are referenced in a $defined. If a method is later added to the parent type, a warning will be issued.

So the check is now tied to whether there is legitimate ambiguity to the $defined, rather than assuming it is wrong if it's invoked out of order.

This model is easy to implement and also greatly improves on the original check.

Integrated MSVC SDK download

C3's long had the ability to cross-compile to MSVC, but now at long last this is no longer supported using a separate script, but everything is integrated into the C3 compiler yielding a silky smooth experience whether just building on Windows or cross-compiling.

Semantics changes

C3's unsigned % signed and unsigned / signed conversions would typically convert the unsigned part to signed. While this is reasonable for other arithmetics, it leads to very surprising behaviour for division/remainder. Since the cases where this happened was very likely to yield buggy behaviour, this is now a hard error.

Cases like unsigned % 1 changes to that the denominator is turned into an unsigned value.

Examples:

int y = 2;
uint x = uint.max / y;  // Invalid, requires explicit cast.
uint y = uint.max / 2;  // Implicitly converts denominator to 2U.
uint z = uint.max / -2; // Invalid, requires explicit cast.  

Warnings

While the C3 compiler has had settings for silencing/enabling warnings through --validation and --silence-deprecation, it didn't have any uniform system for warnings. 0.7.10 changes this and adds --warn-* family of custom settings for individual warnings. Expect this to be expanded on in future versions of the compiler.

Method visibility warnings

Method visibility has been ignored since 0.7.0, but no warning has been issued. Now these warnings have been added, and consequently many stdlib methods have been updated as a result.

struct Foo { int a; }

// @local is ignored, this is a warning in 0.7.10
fn int Foo.test(self) @local
{
    return self.a;
}

fn int Foo.do_something(self)
{
    return self.test();
}

If you were relying on hiding implementation details with local or private methods, use a local / private function instead.

// Using a @local function instead:
fn int _test(Foo self) @local
{
    return self.a;
}

fn int Foo.do_something(self)
{
    return _test(self);
}

Tooling improvements

Android Termux support: has been improved and should now work properly.

Library support: c3c init for libraries now provides helpful examples of exported functions.

Improved Vendor Fetch: c3c vendor-fetch now helpfully lists all packages available from vendor.

Tracking inlining and function sizes: The --print-large-functions has been added. This commandline switch will print out the names of functions that have a large number of instructions. If a seemingly normal function has a large number of instructions, then this signals that the function is likely using too much macro inlining. Aside from longer compile times and larger binary sizes, this will affect the instruction cache, potentially yielding worse performance despite inlining.

'@deprecated' as a contract directive

@deprecated has been available as a contract directive, but it didn't do anything. Starting with 0.7.10 this works properly.

// Deprecation using attribute:
<* 
 Call this old function
*>   
fn void old_test() @deprecated("use new_test")
{ ... }
// Deprecation using contract

<* 
 Call this old function
 @deprecated "use new_test"
*>   
fn void old_test()
{ ... }

Stdlib updates

  • PEM encoding / decoding
  • New hash implementations: Murmur3 and Xorshiro128++
  • Optional line-length cutoff parameter in io::readline
  • array::even, array::odd and array::unlace array filtering functions.
  • Single-byte code page support (DOS/OEM, Windows/ANSI, and ISO/IEC 8859)
  • Discrete and continuous distributions added to std::math

Changes in the stream API

The original stream API used isz and usz for seek and available functions. This has been updated to use 64-bit ints on all platforms. This solves issues working with large files on 32-bit systems.

As part of this, InStream.seek is replaced by set_cursor and cursor.

Notable fixes

  • --cpu-flags didn't work if the first item was an exclusion.
  • Reallocating overaligned memory with the LibcAllocator was unsafe.
  • std::io::Formatter would print incorrect values for values exceeding int128.max.
  • --safe=no would accidentally disable compile-time error reporting on compile-time known runtime @require checks.
  • Member access on a struct returned by an assignment expression, e.g. (foo = bar()).a would cause a crash.

Looking Forward

0.7.11 should bring a healthy number of additions to the stdlib, and there needs to be some early preparation for 0.8.0 as well.

The surrounding tooling is what needs the most attention:

  • Evolving beyond vendor-fetch for retrieving libraries.
  • The need for a SOLID LSP is getting more urgent.
  • An official C3 code formatter is needed.
  • Likewise, an official C3 docgen is getting increasingly urgent.

Community and Contributions

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.

PR contributors for this release:

Stdlib: Book-reader, Fernando López Guevara, konimarti, Laura Kirsch, Manu Linares, mmoustafa8108, soerlemans, Zack Puhl.

Compiler & toolchain: Book-reader, Damien Wilson, Foxy-Boxes, Gantsev Denis, Kiana, Laura Kirsch, Lucas Alves, Manu Linares, Samuel, srkkov

CI/Infrastructure: Manu Linares, Rauny, Smite Rust.

Change Log

Click for full change log

Changes / improvements

  • C3 is now using its own LLVM libraries when building releases.
  • Method resolution and $defined now works together well unless definitions are out of order for real.
  • Improve error message when using functions as values #2856
  • Improve support for Android with Termux.
  • Integrated download of the MSVC SDK when compiling for Windows.
  • For c3c init with library templates, provide example exported functions. #2898
  • unsigned % signed and unsigned / signed is no longer allowed without explicit casts, except for const denominators. #2928
  • New enum associated value syntax.
  • Individual warning settings added.
  • Change typedef and const enums to not convert from literals by default.
  • Add @constinit to allow old typedef behaviour.
  • Include actual element count in the error message when the array initializer size does not match the expected size.
  • Add --print-large-functions for checking which functions likely dominate the compile time.
  • Improve error message when providing alias with a typeid expression where a type was expected. #2944
  • Const enums removed.
  • Constdef declarations introduced.
  • Properly support @deprecated as contract.
  • Support deprecating enum values.
  • Improve error when trying to use an extern const as a compile time constant. #2969
  • vendor-fetch command now lists all available packages by default. #2976
  • Typekind enums are changed CONST_ENUM -> CONSTDEF, DISTINCT -> TYPEDEF.

Stdlib changes

  • Summarize sort macros as generic function wrappers to reduce the amount of generated code. #2831
  • Remove dependency on temp allocator in String.join.
  • Remove dependency on temp allocator in File.open.
  • Added PEM encoding/decoding. #2858
  • Add Murmur3 hash.
  • Add optional line-length limitations to io::readline and io::readline_to_stream. #2879
  • Add Xorshiro128++.
  • Add single-byte code page support (DOS/OEM, Windows/ANSI, and ISO/IEC 8859).
  • Add array::even, array::odd, and array::unlace macros. #2892
  • Add discrete and continuous distributions in std::math::distributions.
  • Add bitorder functions store_le, load_le, store_be, store_le.
  • Stream functions now use long/ulong rather than isz/usz for seek/available.
  • instream.seek is replaced by set_cursor and cursor.
  • instream.available, cursor etc are long/ulong rather than isz/usz to be correct on 32-bit.
  • Enable asynchronous, non-blocking reads of subprocess STDOUT/STDERR pipes on POSIX systems.

Fixes

  • Add error message if directory with output file name already exists
  • Regression where nested lambdas would be evaluated twice.
  • Compiler crash when using arrays of vectors in lists. #2889
  • Fix list[0].i = 5 when list[0] returns a pointer. #2888
  • Shadowing not detected for generic declarations #2876
  • Const inline enums would not always implicitly get converted to the underlying type.
  • Update to dstring.append_string to take any type converting to String.
  • Flag --cpu-flags doesn't work if the first item is an exclusion. #2905
  • Reallocating overaligned memory with the LibcAllocator was unsafe.
  • Using [] or .foo on $$ functions would not raise error but instead crash
  • Improved underlining errors/warnings when unicode is used. #2887
  • Fix std::io::Formatter integer issue for large uint128 decimal values.
  • --safe=no disabled compile-time errors on compile-time known runtime @require checks #2936
  • On assert known false, the message was not shown for no-args.
  • Adding the incorrect sized vector to a pointer vector would cause a crash.
  • Member access on a struct returned by the assignment expression, cause crash #2947
  • Trying to slice an indexable type leads to misleading error message #2958
  • Warn on use of visibility modifiers on methods. #2962
  • Compiler crash using ?? with a void? macro #2973
  • Fix issue when extending a generic type with a method in another module.

Want To Dive Into C3?

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 or Hacker News.