back
▲ 182 points

Show HN: C3 – A C alternative that looks like C

by lerno·4y ago·136 comments·view on hn ↗
Compiler link: https://github.com/c3lang/c3c

Docs: http://www.c3-lang.org

This is my follow-up "Show HN" from roughly a year ago (https://news.ycombinator.com/item?id=27876570). Since then the language design has evolved and the compiler has gotten much more solid.

Assorted extra info:

- The C3 name is a homage to the C2 language project (http://c2lang.org) which it was originally inspired by.

- Although C3 mostly conforms to C syntax, the most obvious change is requiring `fn` in front of the functions. This is to simplify searching for definitions in editors.

- There is a comparison with some other languages here: http://www.c3-lang.org/compare/

- The parts in C3 which breaks C semantics or syntax: http://www.c3-lang.org/changesfromc/

- Aside from the very C-like syntax, one the biggest difference between C3 and other "C competitors" is that C3 prioritizes C ABI compatibility, so that all C3 special types (such as slices and optionals) can be used from C without any effort. C and C3 can coexist nicely in a code base.

- Currently the standard library is not even alpha quality, it's actively being built, but there is a `libc` module which allows accessing all of libc. Raylib is available to use from C3 with MacOS and Windows, see: https://github.com/c3lang/vendor

- There is a blog with assorted articles I've written during the development: https://c3.handmade.network/blog

136 comments
This looks awesome! The main thing holding me back from switching from C++ to C is the lack of type safe generic programming. This language looks like it not only solves that, but adds some other interesting features like defer that I've been wanting to try out :D. It looks like there are some examples of large projects getting successfully compiled by C3 (the vkDoom).

Since this is still only alpha 0.2, I'm curious how stable the compiler is and whether the core language features are subject to change? I'd love to start using this on some projects, but I'm always afraid to adopt a language in its early stages.

You shouldn't use C3 on any larger project, but it could be an option for doing gamejams (with raylib or something else with a C API).

For vkDoom it's not a port to C3. What it demonstrates is instead C <=> C3 interop. I removed some of the central functions from the C code and implemented those in C3. The script compiles the C files into .o files with the normal C compiler, then uses the C3 compiler to compile the .c3 files into .o files. Finally all are linked together into a single binary showing off the simple ABI compatibility story (no extra annotations are needed to ensure compatibility - all C3 functions are automatically callable as C functions)

In regards to the versioning, I've gone through two versioning schemes for the pre-alpha. 0.1.0 is the first version I felt was sufficiently feature complete. Minor version changes (e.g. 0.1.x -> 0.2.x) is for any breaking changes. Minor version doesn't say how close it is to version 1.0 (minor version will continue after 0.9.x with 0.10.x). The 0.1 version was out in April. I try to make the compiler as solid as possible, but I would need thousands (rather than somewhere above 400) tests before I feel confident in the compiler.

Releasing 0.1 means I think that the language design is mostly there now, so fewer changes are coming. But joining any language before 1.0 is a bumpy ride.

Did that answer your question?

Yep! I appreciate the thorough response and I'll be starring the project and following it's progress. It's very cool and looks great so far. Good luck with the project!
Dlang's betterC mode [1] may be what you're looking for; D also has outstanding typesafe template-based metaprogramming.

[1] https://dlang.org/spec/betterc.html

attribute ((overloadable)) in C will name mangle and dispatch using a close approximation to the C++ rules. Much saner in use than _Generic.
Looking at the primer...

    // C
    typedef struct
    {
      int a;
      struct 
      {
        double x;
      } bar;
    } Foo;

    // C3
    struct Foo
    {
      int a;
      struct bar 
      {
        double x;
      }
    }
Very confused by this. The C code declares an anonymous struct type, then aliases the typename "Foo" to that anonymous struct type. The C3 code seems to declares a named struct type "Foo" -- why isn't the C equivalent here just "struct Foo"?

But then within the struct it gets weirder... the C code declares a second anonymous struct, and then declares a member variable of that type. The C3 code... declares a struct named "bar" and also a member variable with name matching the type? Except the primer says that these are equivalent, so the C3 code is declaring an anonymous struct and a member of that type? Using the same syntax as the outer declaration did to declare a named type but no (global) variable?? Is this case sensitive?

I don't think I can get further into the primer than this... even taking the author at their word that the two snippets are equivalent, I don't understand what's in play (case sensitivity? declarations where variable name must match type name?) to make this sane, and there's zero rationale given for these decisions.

> Very confused by this. The C code declares an anonymous struct type, then aliases the typename "Foo" to that anonymous struct type. The C3 code seems to declares a named struct type "Foo" -- why isn't the C equivalent here just "struct Foo"?

I'm curious how familiar you are with C? In C++ you can do:

  struct Foo {
    // ...
  };
  Foo myVar;
But that's not how it works in C. In C this would be:

  struct Foo {
    // ...
  };
  struct Foo myVar;
Which is why many C developers typedef the struct so that they don't have to prefix struct types with the keyword.

> I don't think I can get further into the primer than this... even taking the author at their word that the two snippets are equivalent

Maybe don't judge the author on these things if you're not familiar with how C would work in this case? There's nothing wrong with not understanding a piece of code, but it's generally not a good idea to assume you have understanding of a language like C if you understand C++. People often conflate the two, but there are many quirks of C that C++ doesn't necessarily need to do and vice versa.

Oh gosh... digging further on the same page under "Identifiers" it looks like case sensitivity is the key here. So "struct Foo" declares a type "Foo", and "struct foo" declares a variable "foo" of new anonymous type. I assume "struct Foo foo" and "struct bar Bar" do exactly what you (don't) expect, and maybe even "struct foo bar baz {}" to be the equivalent of the C code "struct {} foo, bar, baz"... yikes.

Edit: "Declaring more than one variable at a time is not allowed." So there's no equivalent to the C code ""struct {} foo, bar, baz"... not clear if "struct IDontNeedANameButTheLanguageIsForcingMe foo {}; IDontNeedANameButTheLanguageIsForcingMe bar; IDontNeedANameButTheLanguageIsForcingMe baz;" is legal (modulo that some of those semicolons are illegal I think?).

Yeah, this needs some rigor in the docs.

> why isn't the C equivalent here just "struct Foo"?

That would not be the equivalent... you would then need to declare the type of Foo variables as:

    struct Foo myVar;
With the typedef, and I assume with C3, you would do the more acceptable:

    Foo myVar;
The first part about the name is just like C++: you use the name without `struct` unlike C where structs has its own namespace. That's what it's meant to illustrate.

The second question is more subtle. In C, the syntax is `struct { ... } [optional member name] ;`. Because there is no anonymous struct at the top level, the anonymous structs inside of a struct has a different syntax, also eschewing the final `;`, changing the syntax to `struct [optional member name] { ... }`. If the C syntax structure is desired a final `;` would be required. This syntax change comes from C2.

I'm puzzled with what the market for these kinds of languages are.

C is sort of a dead end. There is very little innovation there. And that's fine; the users of the language seem to want it that way. They just want to write software the same way they've been doing for the last 20 years. Why would such a conservative user base want to switch to a different language like C3?

Linus once said this about Subversion: "Subversion has been the most pointless project ever started... Subversion used to say, 'CVS done right.' With that slogan there is nowhere you can go. There is no way to do CVS right." Could C3 be the Subversion of programming languages?

> C is sort of a dead end. There is very little innovation there.

C is a small language. There are benefits to that. But it also has a handful of historical oddities. Innovation here means to keep C small while also getting rid of those quirks.

C++ is enormous. Rust is headed in the direction of similar enormity.

> I'm puzzled with what the market for these kinds of languages are.

There's a significant number of C programmers who want something slightly more modern and convenient but don't want to write C++ due to a number of reasons.

I think Zig, D are examples of this niche but syntax wise they don't completely look like C.

I love C because it’s so minimal.

And it’s not a dead end at all - embedded systems, wasm, wasi, really fast things, and aside from assembly it’s one of the first things on newer platforms (risc v for example).

I like Go for the same “try to keep it minimal” reasons, and keen to try Zig when I have some time.

I think the bias you are implying might be misplaced.

> C is sort of a dead

C is like a table saw without a blade guard. Simple yet precise, powerful, flexible, and will cut your fingers off if you aren’t careful.

But it’s often exactly the kind of dangerous saw we need sometimes.

There’s no point in trying to improve it - there are plenty of other, safer saws for that.

> They just want to write software the same way they've been doing for the last 20 years.

Actually I just want to write software the same way I've been doing it for the last 40 years, but otherwise yeah.

Should compare with DasBetterC too!

https://dlang.org/spec/betterc.html

BetterC is very nice. I'd like to imagine BetterC lifted out of the D compiler with its own spec and separate evolution... That would have been ideal for me. But with BetterC as part of the D compiler it will always feel more like a gateway drug for D rather than a language in its own right :D

There are a lot of C compilers out there, people write them for fun. I like the idea that someone would write a C3 compiler "for fun". So limiting implementation complexity is something I have as a goal. What would it take for someone to write a compiler for just the BetterC subset? Would it be doable like implementing C?

That page specifies a rationale of: code loaded from C will not load the D runtime. Is it also possible to provide a C-callable shared D library that initializes the runtime dynamically? Would you then get some missing features like global constructors?

Googling around seems to suggest there is rt_init or Runtime.initialize for that?

I've never used D, I'm just curious about the problem described.

Ps. Goes without saying that i, like others, appreciate your comments here.

Holy crap I’ve been wanting to make basically this exact language for a while now: C with modules and defer!

I am wondering how strings are represented (I dislike the sentinel value scheme we have now), and what the library/locale situation is like (hoping it just says everything is UTF8).

Awesome that someone went and did it for me! The one thing I don’t like is the fn declaration, but reading other comments it makes sense why it’s there and I’m sure I’ll get used to it.

There isn't much of a reason to use anything beyond UTF8 today except for interfacing with code and documents that use UTF16.
Aside - Documentation says:

> It is possible to cast the variant type to any pointer type, which will return null if the types match, or the pointer value otherwise.

That seems backwards to me. Maybe it's just me? Surely if the types match that's when we get the pointer value ?

My main point - Strings

I think at this point good strings are table stakes. If you're a general purpose language, people are going to use strings. Are they going to make a hash table of 3D vectors? Maybe. Are they going to do finite field arithmetic? Maybe. Are they going to need networking? Maybe. But they are going to use strings. C's strings are reprehensibly awful. C++ strings are, astoundingly, both much more complicated and worse in many ways, like there was a competition. You need to be doing a lot better than that IMHO.

I happen to really like Rust's str but I'm not expecting to see something so feature rich in a language like C3. I am expecting a lot more than from a fifty year old programming language though.

Variants... that's a bug in the documentation. Thanks for finding it!

I agree about strings. There are basically two strings we use: the string builder and the "string data" which is any sort of slice of bytes that can be interpreted as a string. But starting from there are a lot of different ways one can name and implement them. I'm doing some experiments and before those are finished I don't have a strong opinion.

One thing to note though is that C3 does not have RAII or move semantics and so the C++ std::string or similar isn't even an option.

Hey, I like any language with this kind of goto:

http://www.c3-lang.org/statements/#nextcase-and-labelled-nex...

> It's also possible to use nextcase with an expression, to jump to an arbitrary case:

    switch (i)
    {
    case 1:
        doSomething();
        nextcase 3; // Jump to case 3
    case 2:
        doSomethingElse();
    case 3:
        nextcase rand(); // Jump to random case
    default:
        libc::printf("Ended\n");
    }
> Which can be used as structured goto when creating state machines.
Awesome stuff. It seems like a C+ instead of C++ to me.

I wish it has class/object/ctor/dtor/RAII though, no need for exception. OOD in C using struct and function pointers are doable but is a bit cumbersome. I don't even need runtime overloading or virtual inheritance or any fancy/advanced features of c++, just a better way to organize code in an OOD style is enough, something like Javascript's class sugar syntax to its prototype object syntax(here will be c++ style to struct+function-pointer-style).

multiple function pointers in a struct is inefficient memory-wise, I think you'd be much better off with a single pointer to a lookup table that can be shared by all instances.

Its even more cumbersome, but its what you pay if you want efficiency in c.

In my experience of using D at work alongside people who barely know how to program (i.e. smart but not culturally a Dev), and people who just didn't know D: if you have smart devs simplicity doesn't matter very much.

If anything it's easier to teach a concept than wait for a programmer to be able to see through a wall of messy simplicity.

I think we do need a language like this, and that Zig/Odin/Myrddin/etc have the issue of having a syntax too different from C (Zig being the least offending; Hare is okay but perhaps a bit too minimal). This is great. Are there plans for a package manager? What's the target stdlib size?

That said, I'm not a big fan of the naming. It irrationally feels kinda hard to justify using a language called "C3" (and I also irrationally like cute names and mascots like Hare has).

There is a dependency relationship defined in C3 for libraries, but there is not (yet anyway) any package manager to actually pull those from a remote repository. It just ensures they are present when using it.

The standard library is far from done, but I imagine it having a common core, supported by all targets. Then a common set of modules that are available on most but not necessarily ALL targets. Outside of that there are “standard library ‘extras’” which will only be of use for certain programs. Beyond that there’s the vendor collection of officially supported external vendor libraries that are not part of the standard library.

Regarding the name, I am not in love with it, but people like my other names even less… Suggestions are always appreciated.
Just take C, remove the integer promotion and implicit casts (it means casting constant literals, yes), except for void* ofc. Remove those horrible __thread, generic, typedef (use the preprocessor for function types?), restrict, all optimization "hints", etc keywords. Finally standardize properly the preprocessor macro with a variadic number of arguments... Split completely the syntax definition from the the runtime lib (the main() function, etc).

It seems there is more to remove from C than to add.

> requiring `fn` in front of the functions. This is to simplify searching for definitions in editors.

You don't have to do that even in C, you can use this style for function defs

  static int
  myfunc(...)
  {
  ...
  }
and search or grep for

  ^myfunc\(
to isolate the definition as opposed to the use.
Interestingly removing `fn` was something I've considered more than once, but I had people ask me to keep it. Even though you can get searchability in C by adhering to certain conventions, that's not the same as being able to search ANY code base in a simple manner, which I believe is what people wanted.
I was confused by this, thought they added `fn` to simplify the parser which to my understanding is why many languages moved away from C style function declarations. I'd rather they move the return type to the end like in Rust and Go for example
Or just use an IDE. It's 2022 people! We don't have to code like it's 1985.
Yeah but that's a very quirky style; I don't want to change the structure of my code just to make it searchable!
A big win C3 explicitly opts out of are integer types named by their size such as u8/u16/u32/.., yet explicitly sizes the types it does have. Another sore point in C: how do expressions involving different integer types work?
Do you wonder about how type promotion rules work in C3? If so then I can explain them as they differ somewhat from C. The reason for not using bitsize numbered types is readability e.g. `for (i8 i = 8; i > 0; i--)`
Looks interesting, thanks! Just curious, any plans for supporting SIMD, similar to gcc's vector extensions ?
Vector types are built in, which you can do operations on, e.g.

    float[<4>] y = { 1, 2, 3, 4 };
    float[<4>] z = { 2, 2, 2, -100 };
    float[<4>] w = y + z;
It's not completely fleshed out with builtins yet, but will be.
Glanced. 17 or 18 years ago I implemented my own programming language too so I can only say: keep on. Just a quick note in terms of Art and Beauty: what I expect from a new language is expressiveness, and encouragement of good programming techniques, so that a code written by an average programmer in the worst mood would not look as a total mess.
what is it with fn()? If we are so much into being terse then int abc() should be just fine. If we need readability than function() instead of fn() would do better.
If you haven't heard of it before, look up the "most vexing parse" for the kind of thing languages are trying to avoid with this syntactic approach. That specific one is a c++ thing, but C also has its own versions of it (that c++ inherits).

Basically the C grammar is only context free if you cheat in the lexer, and any language trying to improve on C is likely to try to avoid that (or just give up and go wild).

From the post:

- Although C3 mostly conforms to C syntax, the most obvious change is requiring `fn` in front of the functions. This is to simplify searching for definitions in editors.

fn() strikes me as a reasonable compromise between those two concerns.
int abc() is not context-free Iirc.