3

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

 1 year ago
source link: https://news.ycombinator.com/item?id=32005678
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

Show HN: C3 – A C alternative that looks like C
132 points by lerno 9 hours ago | hide | past | favorite | 69 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.

s.gif
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?

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

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.

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

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

s.gif
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 ...
s.gif
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…
s.gif
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().
s.gif
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`.
s.gif
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.

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

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

s.gif
> 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;
s.gif
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.
s.gif
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.

s.gif
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...
s.gif
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;
    };
s.gif
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.

s.gif
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?
s.gif
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?

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

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

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

s.gif
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++.
s.gif
> 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.

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

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

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

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

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

That sounds like a library, not a compiler extension.

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

Which is part of the problem! Ugh.

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

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

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

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

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

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.

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

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

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.

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.

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

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

s.gif
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
s.gif
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.
s.gif
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.
s.gif
Yeah but that's a very quirky style; I don't want to change the structure of my code just to make it searchable!
s.gif
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.

s.gif
Or just use an IDE. It's 2022 people! We don't have to code like it's 1985.
s.gif
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.
s.gif
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.

s.gif
Use cscope and/or [ex]ctags and there was no problem in the first place, even in 1985.
s.gif
cscope? Now there’s a name I haven’t heard in 23 years.
s.gif
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.

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

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

s.gif
fn() strikes me as a reasonable compromise between those two concerns.
s.gif
Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search:

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK