Hacker News new | past | comments | ask | show | jobs | submit login
Show HN: C3 – A C alternative that looks like C
182 points by lerno on July 6, 2022 | hide | past | favorite | 136 comments
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




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!


Thank you!


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


I was hoping to see betterC on the C3 comparisons page, but it is compared only to D in the large.


D's betterC is very nice. Very much worth looking at.


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.


> Which is why many C developers typedef the struct so that they don't have to prefix struct types with the keyword.

Oh, hello. Yes, guilty.


Wrapping structs in typedefs like this should be a distant and quaint memory by now. C'mon guys its 2022. Why hasn't some standard gotten rid of this requirement for typedefs around structs?

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.


Changing it would break existing code. In C, types "A" and "struct A" could have been defined as completely different types.


Sounds like: We've always done it this way. Often rephrased as: Its not possible to write a script to fix this. We can't have automated tests. There's no way we can find all the places this is likely to be used. There's no way to do this at all because insert-authority-figure's-opinion. Oh no! But really its all historical rather than a real issue.

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.


That's a strawman. The reason is the option would break code due to C's semantics, in the best case it would fail to compile, and in the worst case it would still compile but change what the code did.

What you're asking for is equivalent to changing the semantics of the language, which is frankly untenable for C.


I'll leave it there. I respectfully agree to disagree. What to you looks like a strawman is for us a signpost towards identifying a module that needs linting and code review. Code that cannot handle that typedef change? Yeah it needs review. What other issues are hiding in there? Oh wait what have they been doing with the preprocessor?

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.



If you can't figure out what vps_t means in your codebase then you have much much bigger issues than typedefs.


I hate when c++ does this. I don't want automatic typedefs, keep the struct so I know what it is. Also causes annoying name conflicts ...


The number of languages that have separate namespaces for the two things is very small. See all the C descendants that discarded that idea: C++, C#, Java, Rust, Go. You probably ought to just find a decent naming scheme to avoid those conflicts…


C++ didn't discard that idea entirely, you can have a struct tag and a function with the same name and you need to be able to do that in order to use stat().


I can’t speak for the others, but Rust definitely has separate namespaces for them. See https://doc.rust-lang.org/beta/reference/names/namespaces.ht...


Not the type of namespace being referred to. In this case, it's referring to the requirement to refer to all types of a certain category with a keyword, as in `struct Foo`. In Rust, you can refer to a struct named Foo as just `Foo`.


I disagree. There may not be a keyword required to refer to `struct`s specifically, but the namespace that is searched when resolving a symbol is determined by the syntax used to refer to it. For example, in C++, you cannot determine whether `a` is a type or a value in the statement `a * b;` without knowing how it is declared, and C also has this ambiguity once `typedef`s are involved. In Rust, there are no such ambiguities (unless there's something I've missed).


c++ just understands that the only purpose you could e using it for is "struct foo". if you have a function "int foo()" as well, then you'll still need the struct tag(which really is necessary because you can do things like call structs like functions)


Why would you want to expose implementation details?


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.


No, you misunderstand. There is no `struct Foo foo`. Unlike C `struct Foo` would only ever be valid at the top level. Neither `struct Foo foo` nor `struct bar Bar` works.

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.


This doesn't seem very well thought out at all


> 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;


From the primer, C3 follows the C++ rules here, not C rules. Changing to using C++ struct/enum/union naming rules here is a much less invasive change than what I'm discussing.


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.


But what if I want to name the inner struct so I can refer to Outer::Inner (for example, for use with sizeof or similar, or to create a temporary to hold a copy of that type) later? Do I need to use typeof(Outer::inner)? And then of course multiple members of the same type...


You can't name the inner struct. So here are the options:

Say that you want this from C:

    struct Foo
    {
      struct Bar {
        int x;
        int y;
      } bar;
    };
    struct Foo f;
    f.bar = (struct Bar) { .x = 123 };
The struct in C3:

    struct Foo
    {
      struct bar {
        int x;
        int y;
      }
    };
    Foo f;
It is correct that we don't get the inner type, but because C3 has inference for {} assignment can happen despite that:

    f.bar = { .x = 123 }; // Valid in C3
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:

    struct HelperStruct
    {
      int x;
      int y;
    }
    HelperStruct test = (HelperStruct)f.bar; // structural cast
But the normal way would be to create a named struct outside if you really need a named struct, e.g.:

    struct Bar { int x, y; }
    struct Foo
    {
      Bar bar;
    };


That ... actually seems reasonable?

If you need it to be first class visible outside, you declare it outside.

I believe I could live with this.


Also, just to be clear, the removal of semicolons deliberately makes the language whitespace sensitive, right? That is,

    struct Foo {
    } foo
declares a variable of a new empty struct type, but

    struct Foo {}
    foo
is a syntax error?


C3 doesn't have declaration of structs/unions in variable declarations. So both are equivalent and are syntax errors.


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.


C is a small language which fails even at the most basic abstractions (you can’t really create a safe and ergonomic “zero-overhead” generic vector type). Due to the inherent complexity of low-level programming, expressive languages in this niche (C++, Rust) has to be reasonably complex just for this reason alone.

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.


C++ is a monsterous cruel joke compared C, but if you’re just looking for some syntactic niceties, nothing stops you from writing C in C++.


C++ -ffreestanding is remarkably usable without the standard library, provided compiler intrinsics are used to fill in some gaps. I'm slowly coming around to using that instead of C for language runtimes.


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


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


> You read 'a=b+c' and you literally have no clue what that means.

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.


fwiw I'm with you even though I've gotten very frustrated with c++'s bloat in other ways. The shift operators were probably a poor choice for iostreams but I don't think the idea was inherently unsound, iostreams is just a very bad library in a bunch of ways.

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.


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


The difference is that you know (or can know, or predict) the outcome. In C++ a=b+c could be a simple addition or an operation that takes an hour, allocates 1GB of RAM, opens a socket and requests a JSON document from a server in China. You can't know wihtout looking at the code.


You don't know what the function called "add" does either.

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.


But with "add" I'm explicitly calling a function.

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


See, framing it as a personal preference makes much more sense. Of course disallowing nontrivial work in operators will also mean you can't concatenate strings in them, so you will have few languages to choose from.


Sorry. I don't know if you are saying that you won't be able to concatenate strings without operators, or if you choose a language just because it allows you to use a "a = b + c" form with strings.

For the first case, of course you can create something like:

    str1.append(str2);
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"?


I never said, that it is impossible to work without operator overloading (actually the opposite). Just that it is a very common feature in most programming languages, and I don't understand why an experienced developer would be confused by it. Even in maths expressions (where the operators come from), you have this behavior. If you multiply two matrices, it does something completely different. The result could be huge and thus in computing world require huge memory allocation. Also you can multiply matrices, but you can't divide them. The primary school level math you seem to be limiting the world to is not practical.


You can predict the outcome very well. It is a call to an operator, which can be trivial for simple types. But of course if you concatenate two 500MB strings using +, you will have a 1GB allocation. I don't see how that is a problem or even a surprise. Of course you could do something in that operator that nobody would expect to happen inside it. But the same is true if you give a function a bad name. For all intents and purposes, an operator is just a function with a particularly short name and (default) style of how it is called.


> You can predict the outcome very well

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.


I don't do embedded (anymore), so please consider that your use-case and concern is very specific.

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.


I was jokingly thinking C++ needs Douglas Crockford to write "C++: The Good Parts" but of course a quick search reveals there already is one

https://www.amazon.com/C-Good-Parts-Gregory-Satir/dp/1449319...


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


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

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.


Please, just no.

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.


> If you want something new, then make a new language.

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)


As someone who actually uses C I think the C standards committee needs to be taken on a one way to trip out behind the barn for the crime of being useless. They've had 30 years to fix things like C not having arrays, lack of modules and name spaces.

Purists can always use std=-c89.


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.


The problem with C is that its... kind of minimal, but it also needs tons of extensions to get things done.

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.


> Want multiple heaps? That'll be a compiler extension.

That sounds like a library, not a compiler extension.


True enough, C doesn't care much about how allocation is done.

Which is part of the problem! Ugh.


Why would C be the requirement for “really fast things”? It doesn’t even have any support for SIMD instructions other than going inline assembly.


Using intrinsics is much more sensible option than dropping to inline asm. Each CPU with SIMD or any kind of specialised instructions provides an intrinsics library for C.


Which, correct me if I'm wrong, is also a compiler extension.


C is a great language. Actually it's not that small, it's just that modern languages often are very big (easy if you compare yourself to C++). The feature bloat is a fairly recent trend.


"C is faster/smaller/... than C++" hasn't been true in a long time.

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++


Personally, I don’t like C++ - maybe it’s the friends function or templating or the usual build systems… and it’s not small by a long shot. My opinion is not really based objectively, I just don’t like it.

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++ compile times would like to have a word with you ;) The biggest drawback of C++ are the atrocious compile times.


Are you serious?

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.


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


This is kinda my opinion too.

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.


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


If the D code is aware that it needs to initialize the runtime then yes, either that you or you initialize it yourself before using it.

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.


The impetus for DasBetterC is to use a subset of D that does not require the D runtime library.

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.


Are they really? Because most c executables are almost never as small as they can be.


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.


If I have that set of semantics I'll use nim compiled to C with ORC attached.

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 actually tried to look for substdio but I failed to get anything good by googling (just that qmail uses it)


Crack open a qmail tarball, it's just a couple files.


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.


I've been thinking of "Xi" or something along those lines. You might also consider something mythological like Odin and Myrddin, or something cute like Hare.


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 doubt many C programmers were waiting for someone to force a function naming style on them after 50 years.


I think this is more aimed at people who couldn't quite get their head around C and were happy to have a forced function naming style that made tooling easier.

(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)


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


Simplifying the parser is cool, but what matters most to me is that I can grep/rg for function definitions. This is really hard in languages that don't have some function keyword.


It simplifies the parser somewhat, but C3 restricts the grammar somewhat from C, so it's possible to unambiguously parse it. However, for people writing tools using `fn` is much easier to parse for. Say you want to write a program that dumps all function names in a file. With `fn` that's a simple regex. Without it you have to do more work.


Or just use an IDE. It's 2022 people! We don't have to code like it's 1985.


I still haven't used a IDE that has made my developer experience better than simple text editor and some cli tools. They all seem to fail spectacularly if you have to do something bit more different than they were designed for (usually regarding how the project is built), and often feel very sluggish. Also the project files or build files they generate are usually horrible, and not great for version control.


Use the IDE for adding an index to your codebase for better navigation & editing and cross file refactoring.

Use whatever headless buildscripts you fancy (make?!) to build your project.

Problem solved.


Code search and an debugger that isn’t a pain to use are the main advantages of an IDE.

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.


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

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.


>Code search

Use grep or ag

>debugger

What's so painful about it?


Grepping and entire code base just to find where some class is defined is certainly doable (I do it often), but it’s not as nice as just right clicking “Go to definition”. Especially in very large codebase with multiple git submodules.

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.


My experience is that they always get in way. Either they slow down the whole thing down, or it randomly freezes. Or the code-completion window decides to popup and it again slows things down.

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 ...)


Use cscope and/or [ex]ctags and there was no problem in the first place, even in 1985.


I don't even bother with code-completion. Not having it makes me actually dig the source code of other components, and is usually helpful deciding if I even want to use the said 3rd party component. Programming is more about thinking than looking up fast what functions to call anyhow.

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 agree. I’m only using “word” completion from open buffers and include lists to save keystrokes. Makes you read, think and choose instead of blindly walking through dots in hope that some combination works. But I also had a good experience with ctags (in 10k+ projects), for quicker navigation.


cscope? Now there’s a name I haven’t heard in 23 years.


Yeah but that's a very quirky style; I don't want to change the structure of my code just to make it searchable!


Sticking fn in front of every def isn't quirkier?

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.


> Sticking fn in front of every def isn't quirkier?

No because almost every single programming language that's not C/C++ has a keyword that signals function declaration.


A good number of those don't have mandated return types, so that keyword is their way of copying c(++)'s look, so its "func main()", like "void main()", instead of just "main()", which to a some people may look strange.


I don't understand why this is relevant. Regardless of the rationale to add it to the language, if syntactically it's `func main()` I can tell it's a function definition because it's always `func`. You can't do that in C++ since it's often some type.


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).


I interpreted the GP to mean: "Why use `fn` instead of `function`?"

After all, he did say:

> If we need readability than function() instead of fn() would do better.


Right, I'm saying that it's not about human readability, it's about parser (and I suppose grep) readability. So the goal is to make the parse easier with as little effort on the part of the programmer as possible.

The whole word function would take up an unnecessarily large part of a line for that purpose.


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.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: