Show HN: C3 – A C alternative that looks like C
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
148 comments
[ 3.1 ms ] story [ 123 ms ] threadBut 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.
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.
The reason why `Bar` is a type and `bar` is a variable or function is to resolve the ambiguity of C syntax without having to rely on unlimited lookahead or the lexer hack. Basically it allows tools to parse the code much more easily than they would C.
You can declare multiple fields in a struct, e.g. `struct Foo { int x, y; }`, but you can't write `int x, y = 123;` like in C. This is because it would create ambiguities in the extended for/while/if syntax C3 adds.
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.
Say that you want this from C:
The struct in C3: It is correct that we don't get the inner type, but because C3 has inference for {} assignment can happen despite that: You can grab the type using typeof if you want, but interestingly there's also the possibility to use structural casts, which are valid in C3: But the normal way would be to create a named struct outside if you really need a named struct, e.g.:If you need it to be first class visible outside, you declare it outside.
I believe I could live with this.
That would not be the equivalent... you would then need to declare the type of Foo variables as:
With the typedef, and I assume with C3, you would do the more acceptable:I'm curious how familiar you are with C? In C++ you can do:
But that's not how it works in C. In C this would be: 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, hello. Yes, guilty.
Deprecate this requirement already and make it an option. Gcc --stupid-typedef-required
Edit: or stated in another way, please describe the good reason(s) for having typedef struct {} Foo in C. Do it in terms of justifying the extra typing / extra steps. I'd really like to know. This is extra code and one more thing to test and confirm. Don't reference history or "its just how its always been done" - that to me is an admisson that it needs to go into an option. Also you'll need to explain why its bad that C++ doesn't require this. I'd also like to see as part of this rebuttal a formal statement that C++ got it completely wrong.
So: Why in C do we require the extra steps just so we can get rid of the struct keyword having to be plastered everywhere?
Personally I think the typedef struct pattern is kept purely for historical reasons and is no longer necessary. It obscures the code rather than clarifies it. There are much better reasons for using typedef but getting rid of "struct" prefixes everywhere isn’t one of them.
Its just a code smell. There are likely other bugs hiding around the code. Business logic issues as well. Overall its a story of generally poorly written and ambiguous code. So... Perhaps fix it?
If you can't fix it then a compiler option would do it.
What you're asking for is equivalent to changing the semantics of the language, which is frankly untenable for C.
The codebases we work on are from the 1980s. They sorely need to be dragged kicking and screaming if necessary to later standards. So many warts. People abused the preprocessor like it was normal and expected.
The semantic change isn’t as big as you'd expect. Even as it is in fact a semantic change. But I sense significant gatekeeping as well to hold back the tide.
You don't have to do that even in C, you can use this style for function defs
and search or grep for to isolate the definition as opposed to the use.Use whatever headless buildscripts you fancy (make?!) to build your project.
Problem solved.
If you wanted code-completion, you'd better use LSP anyways, and you'll get all that fancy compiler diagnostics and whatever in your editor.
I don’t think I’ve ever seen the build and code repo tooling ever work on any professional codebase I’ve worked on, save one.
Use grep or ag
>debugger
What's so painful about it?
No one serious would assert that inspecting complex data structure, managing breakpoints, stepping through code, or debugging remotely via text mode gdb like it’s 1986, is as easy or efficient as simply using a GUI in 2022, or even 1992.
Yeah I agree though in my experience it is because professional teams are usually infected by at least a few "everyone just uses Vim and a terminal right?" people who then go about turning the build system into some horrible script based system that IDEs can't work with.
For navigation I've used something like this since eternity: https://cloudef.pw/armpit/vim-bemenu-curses.mp4
For search, grep or ag will do. As for refactoring, personally I've never had the need for a tooling for that.
>Use whatever headless buildscripts you fancy (make?!) to build your project.
These are just fancy text editors at this point. IDE is like visual studio, or xcode. Sure some of those fancy editors still share the same problems, especially all the java ones (eclipse, android studio, jetbrains ...)
(note, I am -shit- at C, but genuinely wonder if I could write this without spending 98% of my debugging time tracing segfaults that were down to my own stupidity)
Some parts of the kernel use this style.
It's also advantageous because it enables long return types and long function names neatly at the same time.
No because almost every single programming language that's not C/C++ has a keyword that signals function declaration.
- 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.
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).
After all, he did say:
> If we need readability than function() instead of fn() would do better.
The whole word function would take up an unnecessarily large part of a line for that purpose.
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?
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.
This is technically true, but someone that likes writing C while wanting a few extra features would mostly be able to write the same C code they always have if they stick to D's betterC. (I'm not familiar enough with Zig to comment on that.)
Edit: Should also add that D now compiles C code, so a C programmer could continue to write C as they want, write a few D functions where those features help, and compile them without writing any binding code.
That is because C's type declaration format is provably, as in actually provably, terrible.
There is a reason the majority of modern languages have switched away from how C declares types.
Honestly it'd be nice if the C committee could find a way to tack on a new variable declaration syntax to C, so everyone didn't have to keep looking up, or using tools, to declare non-trivial function pointers.
The C committee should do basically nothing. If someone, somewhere, finds an actual bug in the spec, then fine -- fix that. But don't go adding new features.
If you want something new, then make a new language.
Leave C alone.
Purists can always use std=-c89.
Adding an optional new declaration syntax isn't a new feature, it is a change to the parser.
C's declaration syntax is awful. In fact I'd go so far as to claim that one reason function pointers aren't heavily used in C as compared to modern C++, C#, JavaScript, etc, is purely because the syntax around them is so awful.
(They obviously are used, but no one likes using them)
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 don't think this is a fair characterization of rust really. For the most part the things on the horizon still for rust seek to reduce complexity by filing down sharp edges that force you to write complicated code. Stuff like GATs seem complicated until you repeatedly slam your head into the lack of them trying to do things that "seem" natural.
C++ on the other hand (after 2011) just never saw an idiom it didn't like enough to throw on the pile and there's little coherence to the way the language has grown in the last decade.
IMHO it's been incoherent from the earliest times.
The lame exceptions without 'finally', and no consistency in exception types. Then the desperate attempts to make all resources into objects, except that practically no OS calls bothered with this.
The overloading of the shift operators in the standard library. Indeed, operator overloading itself is just a recipe for abuse. You read 'a=b+c' and you literally have no clue what that means.
Multiple inheritance with the brittle semantics.
The awful STL, with its multi-kB error messages (the allocator of the trait of the string of the tree of map of king Caractacus doesn't match ...)
There's no wonder the 'obfuscated C competition' never happened with C++ given the fact it's unreadable, right out of the box.
https://www.amazon.com/C-Good-Parts-Gregory-Satir/dp/1449319...
I never understood that argument. Even in C operators do different things depending on what types you pass it. Two very simple examples:
1. Adding a number to a char* vs adding a number to an int* (or a pointer to any other larger type). The second automatically creates an invisible multiplication. This was confusing for me when I first learned C after already knowing the concept of a memory address (which is just a number). It was an unexpected abstraction for me.
2. This regularly bites novices to programming: Dividing two numbers. If at least one of them is floating point, you get the 'correct' result, overwise rounded towards zero. To 'fix' it, you have to explicitly cast at least one of them to float or double. Then the language imlpicitly casts the other for you. std::pow went the other way, which is less confusing. It always promotes integers to floating point numbers and returns a floating point result.
As soon as your language has types and operators, you get operators that do different things based on the types of the values they are applied to. The only new thing that operator overloading adds is that it makes libraries first class citizens.
There's no reason for the name "+" to be anymore special than "add" - especially in any language supporting unicode identifiers which allows even more crazy names.
> There's no reason for the name "+" to be anymore special than "add"
This is debatable. I want "+" to do just an addition.
Also a function "add", depending on the context, can mean different things.
For the first case, of course you can create something like:
For the second... I don't know what to say.> framing it as a personal preference
But follow my logic for a moment: if "str3 = str1 + str2" concatenates a string, what "str3 = str1 - str2" should do?
(Yes I know there is no minus operator for std::string)
And "str3 = str1 * str2"?
And "str3 = str1 % str2"?
Not always. BTW, I don't know if you noticed it, but I was exagerating to make a point.
> Of course you could do something in that operator that nobody would expect to happen inside it
To me, this is the problem. For example, I do embedded. I have to know with a certain precision what is going on in my program.
It's a very different thing to call a bad-named or badly-done function. It's an unlucky case... I guess?...
But if I use an operator, there might be hidden code there I have to see.
Yes, I know you can use "=" to copy a struct. But also "a = b + c" might be doing something slow, like re-allocating internal pointers and making copies, allocating new objects, etc. I have to read the code to see what it will do. Believe me, I have done it with inherited embedded C++ projects.
But even in C++ you can't redefine what + means for types, where it already has a builtin meaning. The only thing you can do is define what it does for custom types. That means when you see the + operator applied to some class or struct type, you should already be aware that this is nothing built-in and equivalent to a function call. If you are not, you are just not fluent in your tooling. That is completely different problem. It is something learnable, not a fundamental problem of the tooling.
The thing is that in the end, once you have "methods", there are a bunch of reasons you sometimes want to have "infix methods" and for various reasons languages have been reticent to add them as a general concept, so all you get is the operators you have. And it's not like in C++ types are hidden from you most of the time. I guess now that auto is more normalized it happens more but this argument goes back way before auto getting its modern meaning.
But fundamentally, I would rather concat strings with + and I'd rather use normal math operators for vector ops like matrixes or combinatorial concatenation. Every time I see code in a language that doesn't allow you to do this, but people write code doing those things, I cringe at the result.
At a more fundamental level, the fact that struct+struct has no meaning to begin with in C makes it fair game to add it as a concept.
And it's worth noting that addition of pointers is only really a trivial op in non-segmented architectures without any kind of pointer tagging anyways. The window on that assumption opened with 32bit archs and will probably close soon as CPU security primitives evolve.
It was trivial on char* in the 16 bit segmented days (artihmetic was only applied to the offset part of the pointer, if you had a far pointer at all), unless you absolutely needed the huge memory model, which was discouraged anyway because of its slowness. What it did mean though, was that code and data lived in something akin to separate address spaces. But portable modern C code usually doesn't use performance tricks like self-modifying code anymore anyway, because even if it wasn't problematic it wouldn't really be needed anymore.
Zig is an interesting “exception” due to its strong compile-time metaprogramming capabilites, resulting in a small, but quite expressive language. But all 3 has a future. C is here to stay, but I really wouldn’t start any new project in it.
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.
Want multiple heaps? That'll be a compiler extension. Want to specify exactly where a variable is laid out in memory? Compile extension.
Have a memory layout that isn't just a flat map of address space? Better reach for a compiler extension.
Hardware registers? Eh, overlay it with a union or a struct or something, it'll be fine. Unless you want some sort of non-trivial typing on those registers.
People forget that C is written against an abstract C machine. C is not written against the underlying hardware platform. And the abstract C machine may differ from your target hardware by a fair bit.
That sounds like a library, not a compiler extension.
Which is part of the problem! Ugh.
I would argue that the time you need to invest to get something done correctly in C is a cost you can avoid with (modern) C++
I mean, of course, I also like short compile times... but since when are they considered a serious cost? What are we talking about? Seconds? Minutes? Hours?
Are you using your compiler as a syntax checker? Always compiling everything all the time?
Especially since you compile once, what you run a million times and now that everyone is running around with what would have been considered a supercomputer decades ago in their backpack, and a half in their pocket.
Of course I think stuff should compile fast (that's why we use Make, CMake, ninja, etc.) but I would never chose C over C++ because of compile times... Code that requires high mental load and solving the same stupid things again, and again, and again... still costs more time than my compiler can get me back IMO.
If I wanted / needed something more oo I’d probably learn rust.
C++ is everywhere doing amazing things so hate - unreal engine! - just really not my cup of tea.
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.
C is awful, C is terrible, C is almost always the wrong answer.
But when it hits the spot, it's a fantastic wrong answer for a bunch of things.
Actually I just want to write software the same way I've been doing it for the last 40 years, but otherwise yeah.
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.
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?
[1] https://dlang.org/spec/betterc.html
https://dlang.org/spec/betterc.html
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.
A surprising amount of D users like this and use it.
I use it for projects where I want a small runtime executable. DasBetterC executables are as small as C ones can be.
rt_init will fire up the D runtime.
As for constructors the runtime should handle those when the module is initialised, so not necessarily on program start up or the DSO being loaded.
If, however, you want to register a C runtime constructor you can do do so with pragma(crt_constructor).
I actually recently-ish fixed a bug to make the aforementioned type of constructor work on MacOS because Apple decided to deprecate them with no reason.
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?
> 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.
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.
You're aiming for something different, and I appreciate the idea even if I never end up using it.
In the unlikely case you haven't already read it about the only time I got C to do what I wanted without just segfaulting everywhere (my mistakes) was using djb's substdio.
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).
Its even more cumbersome, but its what you pay if you want efficiency in c.
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.
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:
> Which can be used as structured goto when creating state machines.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.
It seems there is more to remove from C than to add.
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).
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.