Hacker Newsnew | past | comments | ask | show | jobs | submit | lerno's commentslogin

I didn't see any similarities to C3, quite the opposite.

C2 (http://c2lang.org) similarly compiles to C, but arguably more readable C code from what I can see. The benefits are (1) easy access to pretty much any platform with little extra work (2) significantly less long term work compared to integrating with LLVM or similar (3) if it's readable enough, it might be submitted as "C code" in working environments which mandate C.

It would be interesting to hear the motivation for it.

But it of course move semantics and destructors affect all things. If the goal is to call and be callable from C without special constructs, how would you make the C code respect the move semantics and destructors?


C++ is already callable from C, you can make functions have any call signature that you want. What is the actual problem?



Please consider a variable `List{int}[3] x`, this is an array of 3 List{int} containing List{int}. If we do `x[1]` we will get an element of List{int}, from the middle element in the array. If we then further index this with [5], like `x[1][5]` we will get the 5th element of that list.

If we look at `int*`, the dereference will peel off the `*` resulting in `int`.

So, the way C3 types are declared is the most inside one is to the left, the outermost to the right. Indexing or dereferencing will peel off the rightmost part.

C uses a different way to do this, we place `*` and `[]` not on the type but on the variable, in the order it must be unpacked. So given `int (*foo) x[4]` we first dereference it (from inside) int[4], then index from the right.

If we wanted to extract a standalone type from this, we'd have `int(*)[4]` for a pointer to an array of 4 integers. For "left is innermost", the declaration would instead be `int[4]*`. If left-is-innermost we can easily describe a pointer to an array of int pointers (which happens in C3 since arrays don't implicitly decay) int*[4]*. In C that be "int*(*)[4]", which is generally regarded as less easy to read, not the least because you need to think of which of * or [] has priority.

That said, I do think that C has a really nice ordering to subscripts, but it was unfortunately not possible to retain it.


Thanks for pointing out what I was missing.

Please consider a variable `List{int}[3] x`, this is an array of 3 List{int} containing List{int}. If we do `x[1]` we will get an element of List{int}, from the middle element in the array. If we then further index this with [5], like `x[1][5]` we will get the 5th element of that list.

I get that motivation. In C++ it's an odd case that where `std::vector<int> x[4]` is "reversed" in a sense compared to `int x[4][100]`. And this quirk is shared with other languages (Java, C#).

But in my experience, mixing generic datatypes like this with arrays is quite rare, and multi-dimensional array like structures with these types is often specified via nesting (`std::vector<std::vector>>`) which avoids confusion.

The argument re. pointers is more convincing though.


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.


I have never found either (1) or (2) to be a problem in hundreds of thousands of lines of Python.

> In particular, C3's "path shortening" ... we'd have to pay by actually writing `std.io.file.open("foo.txt")` or change to a flat module system.

You can easily and explicitly shorten paths in other languages. For example, in Python "from mypackage.mysubpackage import mymodule; mymodule.myfunc()"

Python even gracefully handles name collisions by allowing you to change the name of the local alias, e.g. "from my_other_package.mysubpackage import mymodule as other_module"

I find the "from .. import" to be really handy to understand touchpoints for other modules, and it is not very verbose, because you can have a comma-separated list of things that you are aliasing into the current namespace.

(You can also use "from some_module import *" to bring everything in, which is highly useful for exploratory programming but is an anti-pattern for production software.)


Of course you can explicitly shorten paths. I was talking about C3's path shortening which is doing this for you. This means you do not need to alias imports, which is otherwise how languages do it.

I don't want to get too far into details, but it's understandable that people misunderstand it if they haven't used it, as it's a novel approach not used by any other language.


Oh, I understand it. I just think that (a) explicit is better than implicit; and (b) the amount of characters that Python requires to keep imports explicit is truly minimal, and is a huge aid to figuring out where things came from.


> (1) If I read "io.print", is this "the print function in the module io" or "the print method for the variable io"

I don't see the issue. Just look up the id ? Moreover, if modules are seen as objects, the meaning is quite the same.

> checking is much easier if the namespace is clear from the grammar.

Again (this time by the checker) just look up the symbol table ?


Let's say you have find foo::bar(), then we know that the path is <some path>::foo, the function is `bar` consequently we search for all modules matching the substring ::foo, and depending on whether (1) we get multiple matches (2) we only get a match that is not properly visible (3) we get a match that isn't imported, (4) we get no match or (5) we get a visible match, we print different things. In the case 1-4, we give good errors to allow the user to take the proper action.

If instead we had foo.bar(), we cannot know if this is the method "bar" on local or global foo, or a function "bar()" in a path matching the substring "foo". Consequently we cannot properly issue 4, since we don't know what the intent was.

So far, not so bad. Let's say it's instead foo::baz::bar(). In the :: case, we don't have any change in complexity, we simply match ::foo::baz instead.

However, for foo.baz.bar(), we get more cases, and let us also bring in the possibility of a function pointer being invoked: 1. It is invoking the method bar() on the global baz is a module that ends with "foo" 2. It is calling a function pointer stored in member bar on the global variable baz is a module that ends with "foo" 3. It is calling the function bar() in a module that ends with "foo.baz" 4. It is calling the function pointer stored in the global bar in a module that ends with "foo.baz" 5. It is invoking the method bar on the member baz of the local foo 6. It is calling a function pointer stored in the member bar in the member baz of the local foo

This might seem doable, but note that for every module we have that has a struct, we need to speculatively dive into it to see if it might give a match. And then give a good error message to the user if everything fails.

Note here that if we had yet another level, `foo.bar.baz.abc()` then the number of combinations to search increases yet again.


I think you are overcomplicating this.

This is exactly the syntax Python uses, and there is no "search" per se.

Either an identifier is in the current namespace or not.

And if it is in the current namespace, there can only be one.

The only time multiple namespaces are searched is when you are scoped within a function or class which might have a local variable or member of the same name.

> find foo::bar(), then we know that the path is <some path>::foo, the function is `bar` consequently we search for all modules matching the substring ::foo,

The only reason you need to have a search and think about all the possibilities is that you are deliberately allowing implicit lookups. Again, in Python:

1) Everything is explicit; but 2) you can easily create shorthand aliases when you want.

> note that for every module we have that has a struct, we need to speculatively dive into it to see if it might give a match. And then give a good error message to the user if everything fails.

Only if you rely on search, as opposed to, you know, if you 'import foo' then 'foo' refers to what you imported.


It does compile to WASM.


In C3 it's complicated. On one hand the lack of goto means defers are more straightforward, but the biggest problem is that once you have a different way to handle cleanup and you have labelled break/continue, and you have the nextcase to jump to arbitrary cases, there's very little left for goto to do.

It is limited to when you want to jump from within an if statement out across some statements and run the remaining code. It saves one level of indentation, but being so rare, it's hard to justify the complexity.

I keep going back and see if I find some usecase that could motivate putting goto back in but it so far nothing. The "nextcase" allows C3 to express arbitrary jumps back and forth, although not as straightforward as goto.


Looking at my own code one case I wouldn't get with defer and better switch is avoiding a flag pattern.

For example when you iterate over a block and check if positions are 0 (+ do some work) and once you encounter a non zero you jump to a different "non-empty" section but if it's zeros to the end you jump over non-empty to go to end section. Without goto you need to set a flag and add another conditional there.

Other than that what you mentioned: flatting the if structure is nice. When you have a few simple cases and then a complicated one and a finishing session at the end it's just cleaner and easier to read with goto. It could be handled with a switch statement but not everything is "switchable" and the way most people write it it's another 2 indentation levels (1 with a convention of not indenting cases but I see C3 docs avoid it).

I get it's rare but goto (other than error handling) is rare and I don't think people have a tendency to abuse it. If anything people abuse "structured" construct building an arrow pattern with multiple indentation levels for no good reason.


That's the kind of thing I was thinking about. You can solve that with a switch in C3, but it's not as nice. However, this accounts for no more than 1% of all my goto uses (from a quick inspection), which is too little to build a feature from (discipline is needed to prevent a language from ballooning, it's hard to say no). I am looking for some use for it that can redeem its inclusion.


I agree it's very rare. I have this goto. A few that go to common return block that can be handled by other means and the rest is either error handling or things handled by labelled break or switch.

I mean we know you can program without it and defer/labelled switch and labelled break/continue cover 99%+ of use cases of it. I am still not convinced those are in fact easier to read but I get it's a reasonable design choice to make.


They have different wins. I think labelled break/continue help because they are clearer in locally expressing what the point is. If you see `goto NEXT;` you can kind of guess the intention, but `continue OUTER;` doesn't require you to read the code at the label and check what's happening there. `defer catch` and `defer try` helps avoiding some booleans otherwise needed with just a basic defer. `defer` on its own otherwise sometimes needs booleans to track what should be closed. With goto those naturally go to different cleanup sections.

I keep revisiting goto though. I like it a lot for its simplicity.


Still looking for a good design: https://github.com/c3lang/c3c/issues/829


Did you manage to get more discussion on the Discord?


I don't recall exactly, I think we rehashed the same points.


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

Search: