I keep thinking about perhaps LLMs would make writing code in these lower-level-but-far-better-performing languages in vogue. Why have claude generate a python service when you could write a rust or C3 service with compiler doing a lot of heavy lifting around memory bugs?
I think the same. It sounds quite more practical to have LLMs code in languages whose compilers provide as much compile-time guardrails as possible (Rust, Haskell?). Ironically in some ways this applies to humans writing code as well, but there you run into the (IMO very small) problem of having to write a bit more code than with more dynamic languages.
This is neat and I wish C3 well. But using Nim has shown me the light on maybe the most important innovation I've seen in a native-compiled systems language: Everything, even heap-allocated data, having value semantics by default.
In Nim, strings and seqs exist on the heap, but are managed by simple value-semantic wrappers on the stack, where the pointer's lifetime is easy to statically analyze. Moves and destroys can be automatic by default. All string ops return string, there are no special derivative types. Seq ops return seq, there are no special derivative types. Do you pay the price of the occasional copy? Yes. But there are opt-in trapdoors to allocate RC- or manually-managed strings and seqs. Otherwise, the default mode of interacting with heap data is an absolute breeze.
For the life of me, I don't know why other languages haven't leaned harder into such a transformative feature.
I wonder, at which point it is worth it to make a language? I personally implemented generics, slices and error propagation in C… that takes some work, but doable. Obviously, C stdlib goes to the trash bin, but there is not much value in it anyway. Not much code, and very obsolete.
Meanwhile, a compiler is an enormously complicated story. I personally never ever want to write a compiler, cause I already had more fun than I ever wanted working with distributed systems. While idiomatic C was not the way forward, my choice was a C dialect and Go for higher-level things.
How can we estimate these things? Or let's have fun, yolo?
> I wonder, at which point it is worth it to make a language?
AT ANY POINT.
No exist, nothing, that could yield more improvements that a new language. Is the ONLY way to make a paradigm(shift) stick. Is the ONLY way to turn "discipline" into "normal work".
Example:
"Everyone knows that is hard to mutate things":
* Option 1: DISCIPLINE
* Option 2: you have "let" and you have "var" (or equivalent) and remove MILLIONS of times where somebody somewhere must think "this var mutates or not?".
"Manually manage memory is hard"
* Option 1: DISCIPLINE
* Option 2: Not need, for TRILLONS of objects across ALL the codebases with any form of automatic memory management, across ALL the developers and ALL their apps to very close to 100% to never worry about it
* Option 3: And now I can be sure about do this with more safety and across threads and such
---
Make actual progress with a language is hard, because there is a fractal of competing things that in sore need of improvement, and a big subset of users are anti-progress and prefer to suffer decades of C (example) than some gradual progress with something like pascal (where a "string" exist).
Plus, a language need to coordinate syntax (important) with std library (important) with how frameworks will end (important) with compile-time AND runtime outcomes (important) with tooling (important).
And miss dearly any of this and you blew it.
But, there is not other kind of project (apart from a OS, FileSystem, DBs) where the potential positive impact will extend to the future as much.
At the point you want to interface with people outside of your direct influence. That's the value of a language — a shared understanding.
So long as only you use your custom C dialect, all is fine. Trouble starts when you'd like others to use it too or when you'd like to use libraries written by people who used a different language, e.g. C.
I haven’t tried C3 myself, but I happened to interact a lot with Christopher Lerno, Ginger Bill and multiple Zig maintainers before. Was great to learn that C3, Odin and Zig weren’t competing with each other but instead learn from each other and discuss various trade-offs they made when designing their languages. Generally was a very pleasant experience to learn from them on how and why they implemented building differently or what itch they were scratching when choosing or refusing to implement certain features.
I've been following C3 for sometime now, and I really appreciate the discipline in the design philosophy here.
Neither does it force a new memory model on you, nor does it try to be C++. The killer feature for me is the full ABI compatibility. The fact that I no longer have to write bindings and can just mix C3 files into my existing C build system reduces the friction to near zero.
Kudos to the maintainer for sticking to the evolution, not revolution vision. If you are looking for a weekend language to learn that doesn't require resetting your brain but feels more modern than C99, I highly recommend giving this a shot. Great work by the team.
Is full ABI compatibility important? I'm having a hard time seeing why.
I mean… C isn't even an unsafe language. It's just that C implementations and ABIs are unsafe. Some fat pointers, less insanely unsafe varargs implementations, UBSan on by default, MTE… soon you're doing pretty well! (Exceptions apply.)
I see from `test/test_suite/compile_time_introspection/paramsof.c3t` that there is a way to get names & types of function parameters [1]. The language also seems to support default values { e.g. `int foo(int a, int b = 2) {...}` } and even call with keyword arguments/named parameters [2], but I couldn't find any `defaultof` or similar thing in the code. Does anyone know if this is just an oversight / temporary omission?
I agree it's not the best choice.
I mean it's true that you almost always want fall-through when the body is empty and break where it isn't but maybe it would be better to at least require explicit break (or fall-through keyword) and just make it a compiler error if one is missing and the body is not empty. That would be the least surprising design imo.
I was expecting something very bare-bones, but was pleasantly surprised to see a rich list of new and useful features. And maybe it's not the deepest of things to highlight, but what really made me giggle is how C3's analogue to exceptions are called Excuses.
So far so good. The feature set is bit random though. Things i personally miss is function overloading, default values in parameters and tuple returns.
We have solved the better C issue, but nobody seems keen on solving the better compiler issue
Why do we still have to recompile the whole program everytime we make a change, the only project i am aware of who wants to tackle this is Zig with binary patching, and that's imo where we should focus our effort on..
C3 does look interesting tho, the idea of ABI compatibility with C is pretty ingenious, you get to tap into C's ecosystem for free
IMHO the downsides of tagged unions (e.g. what Rust confusingly calls "enums") are big enough that they should only be used rarely if at all in a systems programming language since they're shoehoerning a dynamic type system concept back into an otherwise statically typed language.
A tagged union always needs at least as much memory as the biggest type, but even worse, they nudge the programmer towards 'any-types', which basically moves the type checking from compile-time to run-time, but then why use a statically typed language at all?
And even if they are useful in some rare situations, are the advantages big enough to justify wasting 'syntax surface' instead of rolling your own tagged unions when needed?
I think there is an important difference here from both Option<T> and Result<T, E>: the C3 optional doesn’t allow an arbitrary error type, it’s just a C-style integer error code. I think that makes a lot of sense and fits perfectly with their “evolution, not revolution” philosophy. And the fact that the syntax is ‘type?’ rather than ‘Optional<type>’ also eases any confusion.
From what I can see there you never write the word “Optional” in your code. This is just what they named the feature which stays close to C semantics without the burden of *out params.
Sounds intriguing. But then, the first thing I noticed in their example is a double-colon scope operator.
I understand that it's part of the culture (and Rust, C#, and many other languages), but I find the syntax itself ugly.
I dunno. Maybe I have the visual equivalent of misophonia, in addition to the auditory version, but :: and x << y << z << whatever and things like that just grate.
I like C. But I abhor C++ with a passion, partly because of what, to me, is jarring syntax. A lot of languages have subsequently adopted this sort of syntax, but it really didn't have that much thought put into it at the beginning, other than that Stroustrup went out of his way to use different symbols for different kinds of hierarchies, because some people were confused.
Think about that. The designer of a language that is practically focused on polymorphism went out of his way to _not_ overload the '.' operator for two things that are about as close semantically as things ever get (hierarchical relationships), simply because some of his customers found that overloading to be confusing. (And yet, '<<' is used for completely disparate things in the same language, but, of course, apparently, that is not at all confusing.)
I saw in another comment here just now that one of the differentiators between zig and C3 is that C3 allows operator overloading.
Honestly, that's in C3's favor (in my book), so why don't they start by overloading '.' and get rid of '::' ?
Two reasons, the second being the important: (1) If I read "io.print", is this "the print function in the module io" or "the print method for the variable io". There tends to be an overlap in naming here so that's a downside (2) parsing and semantic checking is much easier if the namespace is clear from the grammar.
In particular, C3's "path shortening", where you're allowed to write `file::open("foo.txt")` rather than having to use the full `std::io::file::open("foo.txt")` is only made possible because the namespace is distinct at the grammar level.
If we play with changing the syntax because it isn't as elegant as `file.open("foo.txt")`, we'd have to pay by actually writing `std.io.file.open("foo.txt")` or change to a flat module system. That is a fairly steep semantic cost to pay for a nicer namespace separator.
I might have overlooked some options, if so - let me know.
It definitely is possible, because languages like Vlang (for example), are able to use '.' instead of '::'. Always saw this as language creator preference, versus any inescapable technical reason.
86 comments
[ 6.6 ms ] story [ 119 ms ] threadhttps://youtube.com/playlist?list=PLpM-Dvs8t0VYwdrsI_O-7wpo-...
July 2025 (159 comments, 143 points): https://news.ycombinator.com/item?id=44532527
In Nim, strings and seqs exist on the heap, but are managed by simple value-semantic wrappers on the stack, where the pointer's lifetime is easy to statically analyze. Moves and destroys can be automatic by default. All string ops return string, there are no special derivative types. Seq ops return seq, there are no special derivative types. Do you pay the price of the occasional copy? Yes. But there are opt-in trapdoors to allocate RC- or manually-managed strings and seqs. Otherwise, the default mode of interacting with heap data is an absolute breeze.
For the life of me, I don't know why other languages haven't leaned harder into such a transformative feature.
Meanwhile, a compiler is an enormously complicated story. I personally never ever want to write a compiler, cause I already had more fun than I ever wanted working with distributed systems. While idiomatic C was not the way forward, my choice was a C dialect and Go for higher-level things.
How can we estimate these things? Or let's have fun, yolo?
AT ANY POINT.
No exist, nothing, that could yield more improvements that a new language. Is the ONLY way to make a paradigm(shift) stick. Is the ONLY way to turn "discipline" into "normal work".
Example:
"Everyone knows that is hard to mutate things":
* Option 1: DISCIPLINE
* Option 2: you have "let" and you have "var" (or equivalent) and remove MILLIONS of times where somebody somewhere must think "this var mutates or not?".
"Manually manage memory is hard"
* Option 1: DISCIPLINE
* Option 2: Not need, for TRILLONS of objects across ALL the codebases with any form of automatic memory management, across ALL the developers and ALL their apps to very close to 100% to never worry about it
* Option 3: And now I can be sure about do this with more safety and across threads and such
---
Make actual progress with a language is hard, because there is a fractal of competing things that in sore need of improvement, and a big subset of users are anti-progress and prefer to suffer decades of C (example) than some gradual progress with something like pascal (where a "string" exist).
Plus, a language need to coordinate syntax (important) with std library (important) with how frameworks will end (important) with compile-time AND runtime outcomes (important) with tooling (important).
And miss dearly any of this and you blew it.
But, there is not other kind of project (apart from a OS, FileSystem, DBs) where the potential positive impact will extend to the future as much.
So long as only you use your custom C dialect, all is fine. Trouble starts when you'd like others to use it too or when you'd like to use libraries written by people who used a different language, e.g. C.
Neither does it force a new memory model on you, nor does it try to be C++. The killer feature for me is the full ABI compatibility. The fact that I no longer have to write bindings and can just mix C3 files into my existing C build system reduces the friction to near zero.
Kudos to the maintainer for sticking to the evolution, not revolution vision. If you are looking for a weekend language to learn that doesn't require resetting your brain but feels more modern than C99, I highly recommend giving this a shot. Great work by the team.
I mean… C isn't even an unsafe language. It's just that C implementations and ABIs are unsafe. Some fat pointers, less insanely unsafe varargs implementations, UBSan on by default, MTE… soon you're doing pretty well! (Exceptions apply.)
[1] https://github.com/c3lang/c3c/blob/master/test/test_suite/co...
[2] https://c3-lang.org/language-fundamentals/functions/
https://c3-lang.org/language-overview/examples/#enum-and-swi...
Some ways C3 differs from C:
- No mandatory header files
- New semantic macro system
- Module-based namespacing
- Slices
- Operator overloading
- Compile-time reflection
- Enhanced compile-time execution
- Generics via generic modules
- "Result"-based zero-overhead error handling
- Defer
- Value methods
- Associated enum data
- No preprocessor
- Less undefined behavior, with added runtime checks in "safe" mode
- Limited operator overloading (to enable userland dynamic arrays)
- Optional pre- and post-conditions
Why do we still have to recompile the whole program everytime we make a change, the only project i am aware of who wants to tackle this is Zig with binary patching, and that's imo where we should focus our effort on..
C3 does look interesting tho, the idea of ABI compatibility with C is pretty ingenious, you get to tap into C's ecosystem for free
Lisp solved that problem 60 years ago.
A meta answer to your question, I guess.
A tagged union always needs at least as much memory as the biggest type, but even worse, they nudge the programmer towards 'any-types', which basically moves the type checking from compile-time to run-time, but then why use a statically typed language at all?
And even if they are useful in some rare situations, are the advantages big enough to justify wasting 'syntax surface' instead of rolling your own tagged unions when needed?
https://c3-lang.org/language-fundamentals/functions/#functio...
You can name it "Result" or (questionably) "Either."
Not "Option," "Optional," or "Maybe;" those are something else.
Sounds intriguing. But then, the first thing I noticed in their example is a double-colon scope operator.
I understand that it's part of the culture (and Rust, C#, and many other languages), but I find the syntax itself ugly.
I dunno. Maybe I have the visual equivalent of misophonia, in addition to the auditory version, but :: and x << y << z << whatever and things like that just grate.
I like C. But I abhor C++ with a passion, partly because of what, to me, is jarring syntax. A lot of languages have subsequently adopted this sort of syntax, but it really didn't have that much thought put into it at the beginning, other than that Stroustrup went out of his way to use different symbols for different kinds of hierarchies, because some people were confused.
Source: https://medium.com/@alexander.michaud/the-double-colon-opera...
Think about that. The designer of a language that is practically focused on polymorphism went out of his way to _not_ overload the '.' operator for two things that are about as close semantically as things ever get (hierarchical relationships), simply because some of his customers found that overloading to be confusing. (And yet, '<<' is used for completely disparate things in the same language, but, of course, apparently, that is not at all confusing.)
I saw in another comment here just now that one of the differentiators between zig and C3 is that C3 allows operator overloading.
Honestly, that's in C3's favor (in my book), so why don't they start by overloading '.' and get rid of '::' ?
In particular, C3's "path shortening", where you're allowed to write `file::open("foo.txt")` rather than having to use the full `std::io::file::open("foo.txt")` is only made possible because the namespace is distinct at the grammar level.
If we play with changing the syntax because it isn't as elegant as `file.open("foo.txt")`, we'd have to pay by actually writing `std.io.file.open("foo.txt")` or change to a flat module system. That is a fairly steep semantic cost to pay for a nicer namespace separator.
I might have overlooked some options, if so - let me know.