This looks promising. We haven't seen a lot of new languages lately that could turn into viable alternatives to C. Muon binaries don't seem to have a runtime or other dependencies so it looks like a module written in Muon can be statically linked into an existing C/C++ project. Pretty cool.
I've used it for some hobby projects a year or two ago. Unfortunately the compiler was very unstable back then and linking Nim into existing projects turned out to be pretty painful, in part because of the gc/threading model.
There is a lot to like about Nim, but I think it tries to do too much.
But you can turn off the GC and use it as a ‘better C’ (Nim supports raw pointers by the way.) Although you have to ditch most of Nim’s standard library, you can still import C libraries, including libc (I’ve heard you can also do this for the D language.)
Only part of the stdlib depends on GC. Also the GC runs independently in each thread and can work with deadlines, making it harmless in most use-cases.
Yes, one of the goals for Muon is to make it easy to use Muon code from another language.
There's a recent wave of new languages in the same spirit as Muon, like Zig, Kit, Jai and V (the last two have not yet been released). But since they all have no/minimal runtime, it should be relatively easy to reuse libraries between them.
Ah, good question. Rust could definitely be in that list as well! It's a more mature language (so that's why I didn't mention it in the list), but conceptually it very much fits.
Seeing the original list, my guess was that it was no gc, with a small runtime - and the immediate question then was - how is this different/better than "D as a better C"?
The main difference is simplicity. D has a lof of features, like classes, inheritance, exceptions, properties, etc. Muon is a much simpler language that specifically avoids all of those things.
Right now, you'd have to use OS-specific threading APIs for that (via foreign function declarations). I'm planning to add threading support to the standard library to make it easier to write cross-platform multi-threaded code. Also on the roadmap are basic concurrency primitives, like channels.
I haven't decided on what I will do in terms of a memory model: likely, I will add acquire/release constructs to the language, which should be enough to implement things like lockfree data structures as libraries.
I like the emphasis on simplicity. I like the functions, the namespaces, the customizability of allocators, and more. Much respect to the author for creating Muon and sharing it.
What sets Muon apart from all other languages that I've personally tried is that Muon seems to have chosen a unique indentation approach. First, there is a double-syntax for blocks, because Muon requires block indents with significant whitespace (akin to Python) and simultaneously requires block open/close braces (akin to C). Second there is a free choice of tabs or spaces (but not both) and simultaneously a meta-markup line that a developer can put at the top of a file that enables mixing tabs and spaces.
IMHO these kind of whitespace choices seem small at first, yet eventually make open source development more difficult when teams make different choices, and when teaching examples make different choices. I'm a big proponent of `rustfmt`, `gofmt`, and similar tooling that makes it easy to align diverse teams from many groups.
That's a fair point. It's the reason that I've marked significant whitespace as "experimental" in the documentation. Since the language is in an early stage, I think it's OK to try some slightly more experimental constructs for now. The feature does provide some nice benefits in terms of parsing robustness, which is really noticeable especially when working with live error feedback (i.e. language server, coming soon).
But rest assured, this is very much on my radar! If it turns out to be too much of a pain, I'll remove it (or make it opt in). Fortunately, because of the redundant braces the transition would be seamless: we can just drop the whitespace requirement.
I actually think that this is pretty close to the sweet spot. You make line breaks significant, but not the indentation level. The indentation will be inferred automatically from the braces. That way you can paste code that has the wrong indentation, hit save, and as long as the braces are balanced, the indentation will get fixed automatically by an auto-formatter.
I would be very interested to know how happy Python programmers are with the indentation level being significant. Did Guido ever say anything about it?
I worked almost exclusively in python for about five years and loved significant whitespace. It was one of the things I missed most when working in other languages. Now I work primarily in Haskell which has significant whitespace with optional braces and I still love significant whitespace and never use the brace style.
In practice I found the copy/paste problem exceedingly rare to the point that it has been completely a non-issue in both languages.
I think it's a great idea. I think you could perhaps reword it to make things a bit less weird sounding though: blocks are created using curly brackets, but correct indentation is enforced by the compiler. Indenting weirdly (e.g. mixing spaces/tabs) will cause a compile error.
Also I can't see how it would affect code sharing at all. I assume it is per-file, so adding entire files won't matter, and if you have a project that wants to steal a bit of code from another in one file, surely you want to convert their indentation to yours - which would be trivial to do given that you enforce correct indentation.
I think it is a good idea. If you ban tabs you'll piss off the sane 50% of programmers, and if you ban spaces you'll piss off the other 50%. Allowing either but with no mixing makes sense.
The only exception I can think of is that some people like to do tabs for indentation and spaces for alignment. Dunno if you could support that easily...
With regards to your last point, that would be fine. The compiler doesn't care about spaces vs tabs after the first non-whitespace character of a line, as that's not relevant to how the code is parsed.
This looks to have been well thought out. I generally shy away from C/C++, I prefer the GC approach of C#. (I have also coded a little Rust but find it challenging.) But looking at the docs and examples, I'm very tempted to try Muon! I like the lack of undefined behavior very much, even if it only transpiles to C atm; I look forward to the LLVM target.
Uniform call syntax and constructor functions go a long way in making usage more pleasant. I’m curious about “tagged pointers”, though: is the only way to make a “union” by taking the reference of a bunch of things? Seems somewhat inconvenient. Finally, there seem to be a couple of opinionated requirements in the language, namely significant whitespace (made “worse” because you’re already using brackets to define scope) and restricting generic parameters to single-letter names that seem like they’re a bit too restrictive.
Correct, tagged pointers are just for pointer unions, a value union could be emulated using the transmute builtin function. A more general mechanism for discriminated unions is on the roadmap. It would have been nice to include it for the initial version, but I've been in stealth mode for too long already so really needed to launch ;).
I'm not worried about single letter generic param names being too restrictive. Having more than 2 or 3 type params usually implies some fairly heavy abstraction, which is not really in the spirit of the language.
My concern with generic parameters was that I might have situations where both of them start with the same letter so I have to assign an unrelated letter to one of them (for example, say a type generic over an Iterator and Index: one of them can be I and the other one would be…J?)
Good point, I agree that's a bit of an awkward case. It's something I'm planning to keep an eye on. If this becomes a problem in practice, we can do something about it.
I need to get this gripe off my chest. I want types on the left, and not optional.
I read the main function first. I saw that call to countOccurances, without an argument. I was confused how it was going to count occurances if it didn't know what it was looking for. Then I noticed the implementation of this method above.
Had there have been a type of Map<string, int>, I'd have reasoned what it does easily.
Here the problem seems to be especially that the return type is infered. Usually the return type of a function is regarded as belonging to the functions interface and should be accessible without reading the implementation.
Muon always allows you to specify a return type for clarity, though. The key point is that you have the option not to. Not all code is created equal. For library functions that are used by many others it is probably a good idea to add explicit return types, but if you're quickly iterating on a prototype it can be nice to not have to specify them.
I should also add that a language server will really help here. The tooling can just tell you what the return type of a given function is. I can also imagine building some tooling that automatically adds return type annotations if they are missing (which would be quite easy to build). Such a tool could be used as a commit hook to enforce a certain coding standard for teams that would prefer this.
The problem is with contract versioning. If you're writing a library, and your return types are derived rather than explicit, it's way too easy to break your clients by changing the type without even noticing. So maybe require it for public interfaces?
Very cool project! Thank you for taking the time to prepare this reveal so well. Examples and roadmap and all. Good job!
It looks like you started with writing an interpreter in C#, then wrote the compiler in the language itself. Which is a cool concept, I must say. I can't fully wrap my head around the practicalities, though. Do you compile the compiler with an older version of itself during development? Do you plan to update the interpreter as the language evolves?
Builtin serialization of any type Couldn't this be implemented with compile time evaluation in the standard library?
Variable redeclarations Why?
What about a build tool? Is an interface with the compiler going to be part of the compile time evaluation?
How long have you been working on this? Is this your first language project?
It looks like you started with writing an interpreter in C#, then wrote the compiler in the language itself. Which is a cool concept, I must say. I can't fully wrap my head around the practicalities, though. Do you compile the compiler with an older version of itself during development?
This is called bootstrapping [0] and is common in compiler development.
Correct, the initial compiler is built using the C# interpreter; the compiler can then be used to compile the next version of itself. Bugs and backwards incompatible language changes can be a problem though. For that reason, it's a good idea to keep the C# interpreter updated so there's always a safe path to bootstrap from.
Variable redeclarations may be useful in certain scenarios, e.g. redeclaring a variable of type Maybe<T> as T. You're right that serialization may be better implemented as a library using some kind of reflection capability, this needs some more thought. I haven't yet thought about the specifics of build tools, but I very much want to make it easy for people to write tools, and offering the compiler as a library will be part of that.
I started with the initial idea for Muon late 2017/early 2018. Initially, the plan was to create a higher level language (more like C#), but the more I iterated the more I realized that low-level was the way to go. Muon is my first large language project, though I've always been interested in compilers and have written some toy languages in the past.
I'm jealous of that self hosted compiler! For Zig I've mandated that we have to forever maintain both the stage1 C++ compiler as well as the self-hosted one (which is not feature complete yet).
Also I have some bad news for you...
> One major caveat: after the above stages have finished, a C compiler still needs to run to generate the final binary, which usually takes up the most time. The LLVM backend will (hopefully) reduce this.
Anyway, Godspeed. I'll be watching Muon with interest :-)
Edit: one more question, if I may:
> No undefined behavior. Undefined behavior can lead to various, hard-to-spot, bugs. In Muon, all behavior, including platform-specific behavior, is defined.
> The compiler currently outputs C code.This means that we inherit C's undefined behavior model, which goes against the goals listed above! An LLVM backend is in the works which will avoid any undefined behavior.
> High performance. Strive for parity with C.
How are these things to be reconciled? First of all LLVM IR and C have basically the same undefined behavior model. In both C and LLVM IR it's possible to output well-defined code. But more confusing to me is how it's supposed to be fast, if everything is defined?
export fn add1(a: i32, b: i32) bool {
return (a - b) > 0;
}
export fn add2(a: i32, b: i32) bool {
return (a -% b) > 0;
}
In this Zig code, we see 2 implementations of a function. `add1` has undefined behavior if `a - b` overflows. `add2` has defined behavior: wraparound arithmetic. The assembly code, with optimizations on:
add1:
cmp edi, esi
setg al
ret
add2:
sub edi, esi
test edi, edi
setg al
ret
You can see that the version with undefined behavior has 1 fewer instruction. How can you expect to compete with C (or Zig's) performance without undefined behavior?
In addition to the Muon compiler I'm still maintaining a C# interpreter which is used for bootstrapping (just so there's always a safe bootstrapping path), so there's some extra work there. The interpreter only supports a subset of the language, which means that I have to selectively avoid a few language features in the compiler. It's not too bad though.
I've been digging into LLVM over the last week, and I'm slowly coming to that conclusion. I'm tempted to add an x86_64 backend, but there's already so much work to do ;). Perhaps WASM would be an interesting target to add. It's new so it doesn't have too much cruft yet, and it may actually work as an intermediate representation (haven't looked at this at all though).
Re: edit. Integer overflow will be defined as 2-s complement wraparound. In your example, Muon would always emit the extra instruction, even in optimized builds. I must admit that I don't have a good intuition yet for how often these cases happen. If it means that Muon is leaving significant performance gains on the table, I'd be willing to consider adding a construct that would allow undefined behavior for very specific sections of a program to enable these optimizations. But by default, there would not be any undefined behavior.
For a low-level language like this, I think you have to pretty much assume from the get go that anything that has runtime overhead needs an escape hatch, because somebody somewhere might need it. For example, the ability to explicitly request variables or allocated memory to not be zero-initialized can come in handy.
I agree that it's better to default to well-defined, though. That's one thing that I found somewhat questionable about Zig arithmetic operators - why is the shortest and the most obvious one UB on overflow? Sure, it's like C, but in this case that's exactly the problem. IMO, doing things in the most obvious way should always emphasize correctness over speed.
Actually arithmetic is even less defined in Zig than in C! For example, addition with unsigned integers in C is well-defined, but it is (safety-protected) undefined behavior in Zig.
I actually agree with you that the most obvious way should emphasize correctness over speed, and that's why +, -, /, and * operators have language-level assertions to protect against integer overflow - which is almost always a bug.
If Muon only has well-defined arithmetic, then it would not be able to catch this bug:
Test 1/1 contrived example...integer overflow
/home/andy/dev/zig/build/test.zig:11:20: 0x204267 in ??? (test)
return (income - taxes) > cost_of_living;
^
/home/andy/dev/zig/build/test.zig:7:38: 0x2040d8 in ??? (test)
std.testing.expect(!isValidSalary(income, taxes, cost_of_living));
^
It shows you right where the bug is. If the `-` was defined to be twos complement wraparound, isValidSalary would silently return the wrong data!
Note that if you build this zig code in ReleaseFast or ReleaseSmall mode, it would be actually undefined behavior. ReleaseSafe is available as a build mode that has both optimizations and safety on.
You're correct that Muon wouldn't catch that bug, but I think one can make a pretty strong case that it's an anti pattern to use unsigned integers for that kind of stuff anyway. It's actually the reason that the Muon standard library defines Array.count and List.count as _signed_ integers. Even though these fields should clearly never be negative, having them be unsigned would invite exactly the kinds of bugs that you demonstrate in your test case.
As you say, it's off by default in ReleaseFast, which sounds like what most projects would use for shipping code, and what most users would be running. So the safety checks would mostly be confined to debug builds - and from experience with C++, that's not enough. Especially for integer overflow, where it's too easy to write code that works correctly on most inputs, but fails for large values - so those bugs usually manifest in real world use rather than in testing.
I agree that it's better to panic rather than wrap around by default, so long as panic is guaranteed.
Thanks for the compliment. I look forward to watching Muon's progress, and stealing all your good ideas! (I mean that in a friendly way; both Zig and Muon are MIT licensed and of course Zig's ideas are all up for grabs too)
Type names like long, short, char (and the oh-so-dear long long) are ancient cruft which shouldn't be used in a new programming language. They are confusing (as in, the answer to question: how many bits are there in your int, long, short depends on both arch and compiler) and since 70s we discovered it the hard way that they're non-extensible (multiple times, actually!). Similar for the fate of char in the post-ASCII world. C still keeps them for backward compatibility, but types like uint32_t defined in stdint.h is becoming common practice for new code.
The choices are also not compatible with C or Go, adding further to the confusion (Muon's int is 32-bit and long is 64-bit on all archs!), so even for C/C++ and Go programmers, the nostalgia turns into a pitfall. Having no familiarity with C# (not much of a Windows user since late-90s, and C# virtually doesn't exist for Linux/macOS people which Muon claims to be targeting), I looked it up, and it seems it's based on C#'s distorted and incompatible adoption of C89's data types.
Go applies the same custom to float/double as float32/float64 which makes it uniform. Go also defines int to be the size of the native integer register size on the target arch (which I like because this is what int supposed to mean originally, this was the common understanding when porting between C and assembly, although some may disagree after ~50 years of weird adaptations of it) which unifies uint and size_t, rendering size_t obsolete.
Ironically, Muon makes use of explicit number-of-bits suffix for one data type: bool32 (and there's no bool16 or bool64). I don't even know why one might have it as a basic language type.
I really hope he adopts a naming convention that is similar to C99's stdint.h or Go (which the author claims to be inspired by), or their shorter cousins u8, u16, ... used in Linux source code.
Yup, the type names for integers (byte, short, int, long, etc.) are borrowed from C#. All primitives in Muon have a well-defined, platform/architecture independent size. Two exceptions: ssize and usize (machine word sized integers), which are mostly used for C interop.
C# programmers should feel right at home, but you're right that there is a very slight learning curve for others.
But, I hear you! I've personally worked a lot with C# which is why I chose the current names, but I realize that there are others that prefer the explicit names (u8, i8, etc.). I'll be adding a type aliasing feature soon, so anyone who wants to define their own alternative type names will be free to do so. At that point it may also make sense to use explicit names as the default.
Related, and very much a personal preference: Given that there will be no classes etc, isn't the use of camel case (and the not-that-different Pascal Case) for both structs and functions giving up a dimension of distinction?
The ClassCamel and snake_function_case split of Python and Rust is something I bikeshed about often - sooner or later 'IO', 'TCP' or 'MMbT' needs to be CamelCased and then it all goes downhill. Or C and C++ (the latter is also mentioned as an inspiration), which never used CamelCase. I think Java then used it purely to distinguish itself also by style from these direct and unsafe memory poking languages.
I'm not sure if I understood your question correctly, but lowerCamel vs snake_case is not guaranteed to be different right? E.g. if you have an identifier consisting of a single word.
I'd recommend using UpperCamel for namespaces/types (including structs), and either lowerCamel or snake_case for everything else. I personally like that approach because it makes it possible to distinguish between a regular function call and an instance call, e.g.: SomeNamespace.foo() vs someLocalVar.bar().
That said, the language doesn't enforce any particular naming scheme, so people are free to do what they want. Once the type aliasing feature lands, they can even create alternate names for existing types.
I'm surprised that Ada's Camel_Snake_Case isn't more common. It's no more verbose than snake_case but easier to read the words like a natural language and avoids acronym capitalization ambiguities, e.g. "XML_HTTP_Request" instead of "XMLHttpRequest".
I think it's uncommon because it is redundant - if you already have underscores to separate words, why capitalize?
Personally, though, I find casing to be a better way to split up words in languages which have . as a member access operator, for the simple reason that it makes property or method call chains more readable. When you have both dots and underscores in the mix, they both register up as whitespace first, and then you have to distinguish which is which. When it's just dots, it's clear where the boundaries are.
Would C# programmers have any trouble understanding what uint32 etc. are? Whereas, although having long experience with C++, I still feel I always have to reach out for the particular OS's docs to find out what are the specific lengths, again? (For unsigned long, etc.) In other words, I believe explicit naming like uint32 is just universally trivial and ergonomic (including for C# programmers), while short etc. is kinda probably ergonomic for experienced C# programmers (I suppose beginners don't remember it well yet, and also have to consult the manual), while strictly wasting time and brain cycles for programmers coming from other languages :( Please — like, pretty please!...
> Go also defines int to be the size of the native integer register size on the target arch
Which is quite redundant, not to mention causes issues when porting between different architectures. Rust does the right thing by using `isize` and `usize` to make it explicit.
It isn't redundant because that's the only type in Go which does that. There is no issue because int is explicitly defined to mean the CPU word.
As I already mentioned above, due to how its meaning mutated and got distorted over time, it may be better to use a different word. usize/isize is also fine IMO.
Why not call it register size or something like that and then define it .h. Integer means something else for “normal” people, even though after 40 years I get used to it.
> They are confusing (as in, the answer to question: how many bits are there in your int, long, short depends on both arch and compiler) and since 70s we discovered it the hard way that they're non-extensible (multiple times, actually!).
Did we though? The original design, as it was in ALGOL 68, was very much extensible - it specifically said that SHORT and LONG are 1) prefixes, not types, and 2) any amount of them is valid, but the implementation only has to provide SHORT INT, INT and LONG INT, and then the rest are there for shorter and longer implementation-defined types.
What broke it in C mainly that instead of the obvious sequence of short-int-long being 16/32/64 from the get go, they have reused int to mean "most common type". So the original 16-bit architectures had short and int being basically the same, making short redundant. Then on 32-bit, int was changed, but again in a broken way where it was now mapped to long, making that redundant (but still heavily used!). Which set up the 64-bit disaster, where Windows wouldn't make long 64-bit because too much code was out there assuming that it was 32-bit.
If we kept short/int/long separate all along, and didn't try to redefine them afterwards, we wouldn't even need long long today.
I'd rather say having such ambiguous adjectives for types was a bad idea to begin with. And it falls apart when something longer than the original long comes out.
I'm not blaming them for not being explicit about the # of bits back then, though, because back then memory addressing unit (byte) wasn't universally 8-bits and word size wasn't a multiple of 8 either (and some archs weren't even binary): https://en.wikipedia.org/wiki/Word_(computer_architecture)#T...
It seems a naming convention in units of bytes could've been possible though (with the exception of a few archs).
The original design doesn't fall apart, because it allowed multiple prefixes - LONG INT, LONG LONG INT etc - from the get go, specifically so that the list of types could be extended arbitrarily. It's not the most concise or descriptive way to name them, but that's an issue distinct from extensibility.
Personally, I think that rather than focusing on number of bits for numeric types, type systems should focus on the valid range of values, more along the lines of Pascal and Ada.
Short/long were descriptive, meaningful adjectives, and in the context of 3 integers of different sizes, it made sense. "long long" isn't a descriptive or meaningful adjective. It's not even a real word.
Luckily, there are signs that this will stop. Non-standard 128-bit integers are called __int128_t rather than "long long long".
> C# virtually doesn't exist for Linux/macOS people which Muon claims to be targeting
I understand what you're saying, but there exist C# desktop applications for Linux/MacOS for quite a long time - say, Banshee, Tomboy come to mind - and dotnet core should be a decent platform for backend services on these platforms by now.
I'm not saying there are zero C# programs in GNU/Linux distro repos. I'm saying compared to its status in Windows world, it's a niche language with very low penetration. C# never got popular in Linux or BSD environments (partially because how Microsoft was hostile against the entire FOSS when it came out) and I personally don't believe it ever will, given the more recent renaissance of languages which brought us Go, Rust, etc which are safe, compiled and truly cross-platform alternatives to C/C++.
There are a few (GNOME) programs, which can be attributed to Miguel de Icaza. They tried pushing for Mono despite all the FUD surrounding C#, but I can't say they got much far.
I know it's tiny sample but neither I, nor many GNU/Linux users around me know C# or ever needed to use it.
I agree that it hasn't, but I'd be surprised if .NET Core doesn't shift the needle on that a bit over time. Previously, Linux was definitely not first tier in C# support (at least in terms of supporting ecosystem, which is as important as the language itself), but that is changing.
FWIW, Unity and Monogame are two popular game engines which deploy C# code to Mac and Linux using Mono. If a modern game runs on Mac/Linux, there's a high chance it's using C#/Mono. So to me, "niche" doesn't seem like an accurate description.
I don't have to know C# to run a Unity game on Linux, though.
The fact that Unity has an "Export to Linux" button doesn't tell anything about the penetration of C#, as a programming language, in GNU/Linux community. People running binaries don't choose or use languages. They just run the binaries generated by the compilers of the implementation of those languages. It's the developers who use languages.
You can as well mention how many Windows programs can run on Linux and macOS using Wine/Proton, because you're conflating the existence of Linux binaries of Windows games (or iOS/Android games) generated using a multi-platform game-engine (and most likely developed on Windows), with prevalence of C# among GNU/Linux developers.
And this is why I specifically referred to distro repos in my original post, because those are developed by the actual GNU/Linux developer population (mainly for GNU/Linux users).
I agree with you - and that's why I started out with "I understand what you're saying" - that it's not a common choice for Mac and Linux. But then again, I'd argue neither is Go, Rust or certainly Muon.
.Net core is potentially going to fix this though.
As for Go, Rust (and maybe Muon): I believe all of those have rough edges in cross-platform development too. Say - building nice UIs across platforms is still messy as far as I'm aware - and the C# cross-platform apps I mentioned (see: Banshee) struggled with that in the past as well.
Definitely don't want to sound like a C# fanboy, although this is the stack I'm working with. I'm merely saying that is is a viable and improving option, so dismissing it in favor of a completely new language seemed odd. Not to knock Muon, but why can't they both co-exist and be equally viable?
Yes, FOSS in GNU/Linux distros are still written mostly C/C++. And I don't think C/C++ is going anywhere soon. What I'm saying is, Go and Rust are very new, but gaining traction fast for new projects, despite their low penetration as of 2019. I can't say the same thing for any implementation of C#.
The concerns outlined here are still valid for the FOSS community today: https://www.fsf.org/news/dont-depend-on-monohttps://www.fsf.org/news/2009-07-mscp-mono And these are serious concerns given the context of Microsoft's hostile campaigns against FOSS, and EEE tactics against all competitors. What Oracle did with Java after acquiring Sun (Oracle vs Google) only justified and strengthened these concerns.
The way I look at it, it's a risk that is not worth taking, especially given the alternatives today.
I feel like there is a lot of lessons that one MUST take into account if they want to create a new PL. Not getting inspiration from Rust or F# should be a mistake.
Sadly Rust also didn't take inspiration in Go's std library.
I think parent refers to Go std library comprehensiveness.
While Go comes with batteries included, Rust leaves up to community libraries to fill in the blanks for lots of basic functionalities such as regular expression for example.
This language looks incredible. I like the code examples and was really sold when I read its point number 2 about functions and no inheritance.
The only frustration I immediately notice is strong reliance on white space for syntax.
* This makes life harder in a modern world of various operating systems and high distribution. Some means of distribution mutilate white space. XML language design spent enormous effort considering for this.
* Line termination differs by OS. Now it’s largely just posix vs Windows, but there used to be even more variance and there could be more in the future.
* Parsing around white space is severely misunderstood by people who do not write parsers regularly. Just this weekend I encountered unique white space syntax added into React’s JSX to compensate for this that doesn’t exist anywhere else in either JS or XML.
* As somebody who maintains a popular code beautifier I have had to learn the hard way that people really enjoy flexibility around white space. They deliberately use my software instead of, or in addition to, the popular software coming out of Facebook because of the flexibility my software allows. The bottom line for many developers is that code beautification can be fully automated and is better performeded by computers and should never be a factor when authoring code.
* code beautification is also highly subjective. Everybody has different opinions on beauty and easy to read. Providing the means to allow developers to exercise those opinions dynamically and automatically makes people happy. So long as it’s just vanity the code stills executed and reads the same way.
> * This makes life harder in a modern world of various operating systems and high distribution. Some means of distribution mutilate white space. XML language design spent enormous effort considering for this.
Distributing source code with significant white space seems like a totally solved problem to me. When has this come up for you?
> Line termination differs by OS. Now it’s largely just posix vs Windows, but there used to be even more variance and there could be more in the future.
Why would there by more types of line terminators in the future? We already have two ways to do it, which is one too many. Who would add another? And if they did, why would all the text editors and software out there in the world be expected to adopt it?
> * code beautification is also highly subjective. Everybody has different opinions on beauty and easy to read. Providing the means to allow developers to exercise those opinions dynamically and automatically makes people happy. So long as it’s just vanity the code stills executed and reads the same way.
Personally, I don't care about people's subjective opinions or what makes them happy. I just want to be able to easily understand what the code does. I find it easier to understand code when it is formatted in a way that I'm familiar with. If each programmer has their own little formatting idiolect, that makes it harder for me to read their code. So, I think it's great when standardized formatting is built into languages, either with formatters (like go and elixir have) or by having a syntax that enforces uniform formatting (as is the case here).
> Personally, I don't care about people's subjective opinions or what makes them happy. I just want to be able to easily understand what the code does.
I read that as only caring that people are forced to do what makes you happy. To me that sounds extremely selfish, perhaps psychotic. Different things make different people happy. I try to be objective about that.
>* code beautification is also highly subjective. Everybody has different opinions on beauty and easy to read. Providing the means to allow developers to exercise those opinions dynamically and automatically makes people happy. So long as it’s just vanity the code stills executed and reads the same way.
No. Formatters have won. All code should be run through a formatter that contains zero options. The result is the how the code looks. Obviously languages with significant whitespace are more limited in what their formatters can do, but the point about it being subjective is irrelevant.
You forgot to mention why formatters would be superior. I do share your opinion, and the two reasons I can think of are (1) consistency and (2) short-circuits style discussions in code review.
In the 'Glimpse of Muon' example, how does the Map::getOrDefault() function work? Shouldn't it accept 2 args: a key and a default value (if the value associated with the given key is not found)?
1. "Data oriented. Just functions, structs and enums. NO: classes, inheritance, properties, etc."
I think you should allow for struct "inheritance", at least in an Oberon-style manner. There are many cases where having a common "head" between structs is useful (e.g. widgets for a GUI toolkit, object types for a scripting language, etc).
2. "Memory is initialized to zero. Array bounds are checked (can be turned off where needed)."
It is not clear from this, but how can both be disabled in a case-by-case basis? For example in D local variables are initialized to zero by default but you can do something like (IIRC): Foo foo = ? (or = nil, i do not remember) which the compiler interprets as "do not zero-initialize this). Similarly can i do something like array[foo] (bound checked) and array{foo} (non-bounds checked, imaginary syntax)?
3. "An LLVM backend is in the works which will avoid any undefined behavior."
LLVM inherits most of C's undefined behavior, so it will not solve your problem there - you'll need to explicitly generate code that works around undefined behavior. This is a problem that the Free Pascal developers also faced (the language has much less undefined behavior than what LLVM assumes).
4. I am not fan of the language enforcing code style (e.g. "The name of a type parameter must be a single uppercase character." and significant whitespace)
5. Nitpick, but "List<T> struct #RefType { dataPtr pointer count int capacity int }" is a dynamic array, not a list (also, an example of why significant whitespace isn't a great idea :-P)
6. "Type inference for function return values and locals"
I actually prefer to explicitly see the data types passed around, it helps better understand what exactly the code is doing (not just the algorithms, but also the data types which are important) without needing to have too much "implied code" in my mind (i.e. with explicit data types i don't need to "know" that "foo" returns "bar" when looking at its use in a diff (or any other non-syntax highlighted, non-code aware code viewing context), i actually know it because i see it in the code).
(FWIW for this reason i also dislike auto in C++ and similar constructs in other languages - you save a bit of short term typing now, for a lot of long term headache later)
I think you need to make your type system good enough to natively express C types, otherwise the string-based pseudotypes will bite you hard later when you want to move away from using C as a backend.
8. I do not see any mention of a preprocessor, how is code supposed to IFDEF stuff (like, e.g. a commercial program that needs to have something like IFDEF DEMO, or a source file that needs to behave slightly differently if linked as a static library or dynamic library, or the use of two APIs like e.g. IFDEF LINUX, IFDEF WIN32, IFDEF OPENGL, IFDEF VULKAN, etc) or work around language limitations (like the non-availability of inherited structs mentioned above that C programs often work around by using a macro that defines common elements for structs)?
9. "Number literal suffixes"
I'm not fan of the underscore in the suffixes and also i'd avoid the uppercase L and introduce an i (that is 'nop' but could be useful for automatically generated code via scripts and/or macros).
There are other minor nitpicks, but i'd need to actually check it in practice to write more :-P.
1. Right now, you can emulate this with the transmute builtin function. Not yet sure if the additional complexity of this feature would be worth it.
2. Array bounds checks can be avoided by using the unchecked_index builtin. Uninitialized locals are not yet possible, but I think it makes sense to eventually add a construct to allow that, for situations that need every last bit of performance.
3. I've only started to look into LLVM recently, thanks for the heads up.
5. C# programmers would like to disagree with you ;). Note that the formatting issue is not due to significant whitespace, as you'd have the same issue writing up a Golang snippet.
6. Editor support (specifically, language server) should help a lot here.
7. The string based types are only used for inserting the right cast to make the C compiler happy, so they should not be an obstacle.
8. Compile time evaluation can be used for some cases. Adding a preprocessor comes with its own set of tradeoffs, haven't yet decided what to do there.
9. A lowercase l looks a lot like a 1 in many fonts, hence the uppercase L. Will consider adding an i suffix.
1. The point of this is to avoid repeating the common headers and have the type system validate that a pointer/reference of a subtype is compatible with its parent type (in C you'd need to typecast which can be dangerous).
5. I also tend to name dynarrays as lists, but in this case you have both List and Array and the only difference seems to be that one has a capacity while the other doesn't. Having both can be a bit confusing when you actually wanted a (linked) list.
6. It will only help when inside the editor, hence why i explicitly wrote "when looking at its use in a diff (or any other non-syntax highlighted, non-code aware code viewing context)".
8. None of the examples i mentioned can be solved with compile-time evaluation (except some cases of IFDEF DEMO) since they change what code is actually compiled (e.g. if you want to use an API that may not even be available on a target - like e.g. DirectX on Linux) or generate code themselves.
9. I do not think it makes much sense to worry about that, there are fonts that have 0 and O look the same, let the user fix their fonts, do not punish those who use unambiguous fonts with inconsistency. Trust your users.
1. That's true, I didn't mean to imply that transmute is equivalent, there would definitely be benefits to the feature you're suggesting. But it also brings additional complexity of course.
6. Sorry, I totally missed that part of your comment! A language server won't help in that case. At least wrt to return types though, you're always allowed to specify them for clarity. I can imagine building some tooling that automatically infers and adds explicit return type annotations if they are missing. You could run this as a commit hook if you wanted to enforce that as a coding standard.
8. Right, I was referring to the IFDEF DEMO case (assuming it's used to implement straightforward feature flags). I agree that for the other cases that you mentioned we'd need something else. For now, for some situations, you could compile with different files (e.g. gfx_opengl.mu, gfx_directx.mu, etc.), though it's a pretty crude solution, especially if you want to combine conditions (e.g. Linux + OpenGL). Definitely thinking about this.
I guess a killer feature would be an integrated "package" manager like .NET's NuGet. Being able to easily "import" C dependencies that way, along with other projects written in Muon could make life much easier. I'm thinking like Rust's Cargo, but without the nonsense of having to build all the dependencies oneself which takes time and storage space - just using a pre-built versioned release would make a massive difference to workflows and reduce friction.
Is anything like this in the works/on the roadmap? (I didn't see it but maybe I just missed it, while browsing on mobile ;))
Short term, I have some tools planned for automatic generation of foreign function definitions based on .h files (currently you have to do this by hand, which is tedious and also somewhat error prone).
Long term, I agree that a package/dependency manager would be great! There's already so much work to do though, so this will unfortunately be out of scope for the near future.
Understood, thanks :)
it'd be great to have an issue on GitHub I could subscribe to for notifying me when your auto FFI generation tool is ready, it would give me a greater incentive to try Muon ;)
This is a really interesting project and I like where you're taking it. I've played with Nim for a number of years, and I really like the no-runtime aspect of Muon and the flexibility and simplicity that you've created with it. Unlike many others in this thread, I like the whitespace decisions you've made, though I go in the opposite direction - have you thought about making braces optional?
Your roadmap looks great as well. Have you considered any of the following?:
- Lisp-style Macros (e.g. Macros as functions that transform an AST object) -- this would also answer many of the
preprocessor questions you had, especially if you can specify compile-time behavior (like in zig)
- Github-based decentralized package management system
Thanks! I don't think we should lose the braces. Braces help avoid mistakes when refactoring/copying/pasting code, and they make the language look more familiar to people coming from C-like languages, which I think are large enough advantages to justify having them.
I'm considering adding some meta-programming/reflection capabilities in some form, if they don't increase the complexity of the language too much. Lisp-style macros would be a possible approach, compiler plugins would be another. The latter approach might be easier to implement though.
A package manager will be out of scope for at least the near future, but long term it would be great to have one!
Re: owned pointers. I'd recommend that people look into more coarse grained memory management strategies first (e.g. arena allocation), and use one of those if possible. However, these strategies definitely have their limitations, which is where alternatives like owned/shared pointers could come in. However, to implement a "proper" owned pointer, e.g. an alternative to std::unique_ptr<T>, we would need destructors (which Muon will never implement) and move semantics (of which there is a chance that Muon _might_ implement it, but probably not anytime soon). With move semantics we can perhaps create a lighter weight owned pointer that is still useful; I'll have to experiment a bit to see how that would work out.
One thing that is missing from Go is computation expressions (in C# 7.0 async/await was generalized to a computation expression). Computation expressions are extremely powerful feature in F# which introduced the idea of async/await. If it is generalized then you can properly handle code failures better.
In the Go community they have found returning the result/error tuple is tedious enough that people don't properly handle their errors. I would argue this is because of the lack of generics and computation expressions.
In F# I can do something like this:
let result : Task<Result<MyObject, Error> = asyncResult {
let! validatedObject = validateObject object
let! dbObject = GetSomethingFromTheDatabase object
return dbObject
}
doSideAffectLastCodeAndHandleAnyErrors result
This lets you create little DSLs and code that you can handle errors in a central location and in your main code just get the work done without thinking about it. Makes for a really great workflow.
116 comments
[ 2.8 ms ] story [ 172 ms ] threadThere is a lot to like about Nim, but I think it tries to do too much.
Some people will argue it isn't a contender for C though since it defaults to using garbage collection.
There's a recent wave of new languages in the same spirit as Muon, like Zig, Kit, Jai and V (the last two have not yet been released). But since they all have no/minimal runtime, it should be relatively easy to reuse libraries between them.
On a more serious note, as a particle physicist, I hope this trend of naming software after fundamental particles ends soon...
On that topic I made this a while ago to make fun of it. (no longer renders nicely on mobile unless you go landscape)
I haven't decided on what I will do in terms of a memory model: likely, I will add acquire/release constructs to the language, which should be enough to implement things like lockfree data structures as libraries.
What sets Muon apart from all other languages that I've personally tried is that Muon seems to have chosen a unique indentation approach. First, there is a double-syntax for blocks, because Muon requires block indents with significant whitespace (akin to Python) and simultaneously requires block open/close braces (akin to C). Second there is a free choice of tabs or spaces (but not both) and simultaneously a meta-markup line that a developer can put at the top of a file that enables mixing tabs and spaces.
IMHO these kind of whitespace choices seem small at first, yet eventually make open source development more difficult when teams make different choices, and when teaching examples make different choices. I'm a big proponent of `rustfmt`, `gofmt`, and similar tooling that makes it easy to align diverse teams from many groups.
But rest assured, this is very much on my radar! If it turns out to be too much of a pain, I'll remove it (or make it opt in). Fortunately, because of the redundant braces the transition would be seamless: we can just drop the whitespace requirement.
I would be very interested to know how happy Python programmers are with the indentation level being significant. Did Guido ever say anything about it?
In practice I found the copy/paste problem exceedingly rare to the point that it has been completely a non-issue in both languages.
Also I can't see how it would affect code sharing at all. I assume it is per-file, so adding entire files won't matter, and if you have a project that wants to steal a bit of code from another in one file, surely you want to convert their indentation to yours - which would be trivial to do given that you enforce correct indentation.
I think it is a good idea. If you ban tabs you'll piss off the sane 50% of programmers, and if you ban spaces you'll piss off the other 50%. Allowing either but with no mixing makes sense.
The only exception I can think of is that some people like to do tabs for indentation and spaces for alignment. Dunno if you could support that easily...
Make a "mufmt" with zero options and let everyone focus on important things.
Uniform call syntax and constructor functions go a long way in making usage more pleasant. I’m curious about “tagged pointers”, though: is the only way to make a “union” by taking the reference of a bunch of things? Seems somewhat inconvenient. Finally, there seem to be a couple of opinionated requirements in the language, namely significant whitespace (made “worse” because you’re already using brackets to define scope) and restricting generic parameters to single-letter names that seem like they’re a bit too restrictive.
Re: whitespace, see my comment here: https://news.ycombinator.com/item?id=19599908
I'm not worried about single letter generic param names being too restrictive. Having more than 2 or 3 type params usually implies some fairly heavy abstraction, which is not really in the spirit of the language.
I read the main function first. I saw that call to countOccurances, without an argument. I was confused how it was going to count occurances if it didn't know what it was looking for. Then I noticed the implementation of this method above.
Had there have been a type of Map<string, int>, I'd have reasoned what it does easily.
I hate type inference, it does not help.
I should also add that a language server will really help here. The tooling can just tell you what the return type of a given function is. I can also imagine building some tooling that automatically adds return type annotations if they are missing (which would be quite easy to build). Such a tool could be used as a commit hook to enforce a certain coding standard for teams that would prefer this.
It looks like you started with writing an interpreter in C#, then wrote the compiler in the language itself. Which is a cool concept, I must say. I can't fully wrap my head around the practicalities, though. Do you compile the compiler with an older version of itself during development? Do you plan to update the interpreter as the language evolves?
Builtin serialization of any type Couldn't this be implemented with compile time evaluation in the standard library?
Variable redeclarations Why?
What about a build tool? Is an interface with the compiler going to be part of the compile time evaluation?
How long have you been working on this? Is this your first language project?
This is called bootstrapping [0] and is common in compiler development.
[0] https://en.wikipedia.org/wiki/Bootstrapping_(compilers)
Correct, the initial compiler is built using the C# interpreter; the compiler can then be used to compile the next version of itself. Bugs and backwards incompatible language changes can be a problem though. For that reason, it's a good idea to keep the C# interpreter updated so there's always a safe path to bootstrap from.
Variable redeclarations may be useful in certain scenarios, e.g. redeclaring a variable of type Maybe<T> as T. You're right that serialization may be better implemented as a library using some kind of reflection capability, this needs some more thought. I haven't yet thought about the specifics of build tools, but I very much want to make it easy for people to write tools, and offering the compiler as a library will be part of that.
I started with the initial idea for Muon late 2017/early 2018. Initially, the plan was to create a higher level language (more like C#), but the more I iterated the more I realized that low-level was the way to go. Muon is my first large language project, though I've always been interested in compilers and have written some toy languages in the past.
Some differences between Muon and Zig:
- Syntax/ergonomics: Muon has return type inference, Go-like syntax/no semicolons, no "pub fn"s anywhere, reference type notation.
- Memory management philosophy: Muon makes it easy to allocate memory, e.g. via "new" operator and fully customizable thread-local allocators.
- Minimal error handling: Muon provides abandonment (i.e. panic), but no extra error handling features like Zig does.
- Muon is a more minimal language in general: no meta programming (yet, at least), no async, no bit fields.
Zig is a lot more mature though, so I'm trying to catch up as fast as I can ;).
Also I have some bad news for you...
> One major caveat: after the above stages have finished, a C compiler still needs to run to generate the final binary, which usually takes up the most time. The LLVM backend will (hopefully) reduce this.
Nope. LLVM is about 80% of the time that it takes to compile Zig code. Other language authors have found similar results. See for example http://pling.jondgoodwin.com/post/compiler-performance/
Anyway, Godspeed. I'll be watching Muon with interest :-)
Edit: one more question, if I may:
> No undefined behavior. Undefined behavior can lead to various, hard-to-spot, bugs. In Muon, all behavior, including platform-specific behavior, is defined.
> The compiler currently outputs C code.This means that we inherit C's undefined behavior model, which goes against the goals listed above! An LLVM backend is in the works which will avoid any undefined behavior.
> High performance. Strive for parity with C.
How are these things to be reconciled? First of all LLVM IR and C have basically the same undefined behavior model. In both C and LLVM IR it's possible to output well-defined code. But more confusing to me is how it's supposed to be fast, if everything is defined?
For example: https://godbolt.org/z/7d8RLG
In this Zig code, we see 2 implementations of a function. `add1` has undefined behavior if `a - b` overflows. `add2` has defined behavior: wraparound arithmetic. The assembly code, with optimizations on: You can see that the version with undefined behavior has 1 fewer instruction. How can you expect to compete with C (or Zig's) performance without undefined behavior?In addition to the Muon compiler I'm still maintaining a C# interpreter which is used for bootstrapping (just so there's always a safe bootstrapping path), so there's some extra work there. The interpreter only supports a subset of the language, which means that I have to selectively avoid a few language features in the compiler. It's not too bad though.
I've been digging into LLVM over the last week, and I'm slowly coming to that conclusion. I'm tempted to add an x86_64 backend, but there's already so much work to do ;). Perhaps WASM would be an interesting target to add. It's new so it doesn't have too much cruft yet, and it may actually work as an intermediate representation (haven't looked at this at all though).
Re: edit. Integer overflow will be defined as 2-s complement wraparound. In your example, Muon would always emit the extra instruction, even in optimized builds. I must admit that I don't have a good intuition yet for how often these cases happen. If it means that Muon is leaving significant performance gains on the table, I'd be willing to consider adding a construct that would allow undefined behavior for very specific sections of a program to enable these optimizations. But by default, there would not be any undefined behavior.
I agree that it's better to default to well-defined, though. That's one thing that I found somewhat questionable about Zig arithmetic operators - why is the shortest and the most obvious one UB on overflow? Sure, it's like C, but in this case that's exactly the problem. IMO, doing things in the most obvious way should always emphasize correctness over speed.
I actually agree with you that the most obvious way should emphasize correctness over speed, and that's why +, -, /, and * operators have language-level assertions to protect against integer overflow - which is almost always a bug.
If Muon only has well-defined arithmetic, then it would not be able to catch this bug:
In Zig the output is: It shows you right where the bug is. If the `-` was defined to be twos complement wraparound, isValidSalary would silently return the wrong data!Note that if you build this zig code in ReleaseFast or ReleaseSmall mode, it would be actually undefined behavior. ReleaseSafe is available as a build mode that has both optimizations and safety on.
I agree that it's better to panic rather than wrap around by default, so long as panic is guaranteed.
https://c9x.me/compile/
Type names like long, short, char (and the oh-so-dear long long) are ancient cruft which shouldn't be used in a new programming language. They are confusing (as in, the answer to question: how many bits are there in your int, long, short depends on both arch and compiler) and since 70s we discovered it the hard way that they're non-extensible (multiple times, actually!). Similar for the fate of char in the post-ASCII world. C still keeps them for backward compatibility, but types like uint32_t defined in stdint.h is becoming common practice for new code.
The choices are also not compatible with C or Go, adding further to the confusion (Muon's int is 32-bit and long is 64-bit on all archs!), so even for C/C++ and Go programmers, the nostalgia turns into a pitfall. Having no familiarity with C# (not much of a Windows user since late-90s, and C# virtually doesn't exist for Linux/macOS people which Muon claims to be targeting), I looked it up, and it seems it's based on C#'s distorted and incompatible adoption of C89's data types.
Go applies the same custom to float/double as float32/float64 which makes it uniform. Go also defines int to be the size of the native integer register size on the target arch (which I like because this is what int supposed to mean originally, this was the common understanding when porting between C and assembly, although some may disagree after ~50 years of weird adaptations of it) which unifies uint and size_t, rendering size_t obsolete.
Ironically, Muon makes use of explicit number-of-bits suffix for one data type: bool32 (and there's no bool16 or bool64). I don't even know why one might have it as a basic language type.
I really hope he adopts a naming convention that is similar to C99's stdint.h or Go (which the author claims to be inspired by), or their shorter cousins u8, u16, ... used in Linux source code.
C# programmers should feel right at home, but you're right that there is a very slight learning curve for others.
But, I hear you! I've personally worked a lot with C# which is why I chose the current names, but I realize that there are others that prefer the explicit names (u8, i8, etc.). I'll be adding a type aliasing feature soon, so anyone who wants to define their own alternative type names will be free to do so. At that point it may also make sense to use explicit names as the default.
The ClassCamel and snake_function_case split of Python and Rust is something I bikeshed about often - sooner or later 'IO', 'TCP' or 'MMbT' needs to be CamelCased and then it all goes downhill. Or C and C++ (the latter is also mentioned as an inspiration), which never used CamelCase. I think Java then used it purely to distinguish itself also by style from these direct and unsafe memory poking languages.
I'd recommend using UpperCamel for namespaces/types (including structs), and either lowerCamel or snake_case for everything else. I personally like that approach because it makes it possible to distinguish between a regular function call and an instance call, e.g.: SomeNamespace.foo() vs someLocalVar.bar().
That said, the language doesn't enforce any particular naming scheme, so people are free to do what they want. Once the type aliasing feature lands, they can even create alternate names for existing types.
Personally, though, I find casing to be a better way to split up words in languages which have . as a member access operator, for the simple reason that it makes property or method call chains more readable. When you have both dots and underscores in the mix, they both register up as whitespace first, and then you have to distinguish which is which. When it's just dots, it's clear where the boundaries are.
Which is quite redundant, not to mention causes issues when porting between different architectures. Rust does the right thing by using `isize` and `usize` to make it explicit.
Muon.ssize == Rust.isize == Go.int ('usually', according to https://tour.golang.org/basics/11)
Muon.usize == Rust.usize == Go.uint
Muon.long == Rust.i64 == Go.int64
This is a list of all core types in Muon: https://github.com/nickmqb/muon/blob/master/docs/muon_by_exa...
As I already mentioned above, due to how its meaning mutated and got distorted over time, it may be better to use a different word. usize/isize is also fine IMO.
Did we though? The original design, as it was in ALGOL 68, was very much extensible - it specifically said that SHORT and LONG are 1) prefixes, not types, and 2) any amount of them is valid, but the implementation only has to provide SHORT INT, INT and LONG INT, and then the rest are there for shorter and longer implementation-defined types.
What broke it in C mainly that instead of the obvious sequence of short-int-long being 16/32/64 from the get go, they have reused int to mean "most common type". So the original 16-bit architectures had short and int being basically the same, making short redundant. Then on 32-bit, int was changed, but again in a broken way where it was now mapped to long, making that redundant (but still heavily used!). Which set up the 64-bit disaster, where Windows wouldn't make long 64-bit because too much code was out there assuming that it was 32-bit.
If we kept short/int/long separate all along, and didn't try to redefine them afterwards, we wouldn't even need long long today.
I'm not blaming them for not being explicit about the # of bits back then, though, because back then memory addressing unit (byte) wasn't universally 8-bits and word size wasn't a multiple of 8 either (and some archs weren't even binary): https://en.wikipedia.org/wiki/Word_(computer_architecture)#T...
It seems a naming convention in units of bytes could've been possible though (with the exception of a few archs).
Personally, I think that rather than focusing on number of bits for numeric types, type systems should focus on the valid range of values, more along the lines of Pascal and Ada.
Short/long were descriptive, meaningful adjectives, and in the context of 3 integers of different sizes, it made sense. "long long" isn't a descriptive or meaningful adjective. It's not even a real word.
Luckily, there are signs that this will stop. Non-standard 128-bit integers are called __int128_t rather than "long long long".
I understand what you're saying, but there exist C# desktop applications for Linux/MacOS for quite a long time - say, Banshee, Tomboy come to mind - and dotnet core should be a decent platform for backend services on these platforms by now.
There are a few (GNOME) programs, which can be attributed to Miguel de Icaza. They tried pushing for Mono despite all the FUD surrounding C#, but I can't say they got much far.
I know it's tiny sample but neither I, nor many GNU/Linux users around me know C# or ever needed to use it.
If I developed a game I would 100% use mono + xna now on OSX.
The fact that Unity has an "Export to Linux" button doesn't tell anything about the penetration of C#, as a programming language, in GNU/Linux community. People running binaries don't choose or use languages. They just run the binaries generated by the compilers of the implementation of those languages. It's the developers who use languages.
You can as well mention how many Windows programs can run on Linux and macOS using Wine/Proton, because you're conflating the existence of Linux binaries of Windows games (or iOS/Android games) generated using a multi-platform game-engine (and most likely developed on Windows), with prevalence of C# among GNU/Linux developers.
And this is why I specifically referred to distro repos in my original post, because those are developed by the actual GNU/Linux developer population (mainly for GNU/Linux users).
.Net core is potentially going to fix this though.
As for Go, Rust (and maybe Muon): I believe all of those have rough edges in cross-platform development too. Say - building nice UIs across platforms is still messy as far as I'm aware - and the C# cross-platform apps I mentioned (see: Banshee) struggled with that in the past as well.
Definitely don't want to sound like a C# fanboy, although this is the stack I'm working with. I'm merely saying that is is a viable and improving option, so dismissing it in favor of a completely new language seemed odd. Not to knock Muon, but why can't they both co-exist and be equally viable?
The concerns outlined here are still valid for the FOSS community today: https://www.fsf.org/news/dont-depend-on-mono https://www.fsf.org/news/2009-07-mscp-mono And these are serious concerns given the context of Microsoft's hostile campaigns against FOSS, and EEE tactics against all competitors. What Oracle did with Java after acquiring Sun (Oracle vs Google) only justified and strengthened these concerns.
The way I look at it, it's a risk that is not worth taking, especially given the alternatives today.
edit: just saw your relevant comment :)
Sadly Rust also didn't take inspiration in Go's std library.
golang's stdlib is nothing special, and even has bad practices, such as weird casting to arbitrary interfaces and hoping it does the right thing.
While Go comes with batteries included, Rust leaves up to community libraries to fill in the blanks for lots of basic functionalities such as regular expression for example.
The only frustration I immediately notice is strong reliance on white space for syntax.
* This makes life harder in a modern world of various operating systems and high distribution. Some means of distribution mutilate white space. XML language design spent enormous effort considering for this.
* Line termination differs by OS. Now it’s largely just posix vs Windows, but there used to be even more variance and there could be more in the future.
* Parsing around white space is severely misunderstood by people who do not write parsers regularly. Just this weekend I encountered unique white space syntax added into React’s JSX to compensate for this that doesn’t exist anywhere else in either JS or XML.
* As somebody who maintains a popular code beautifier I have had to learn the hard way that people really enjoy flexibility around white space. They deliberately use my software instead of, or in addition to, the popular software coming out of Facebook because of the flexibility my software allows. The bottom line for many developers is that code beautification can be fully automated and is better performeded by computers and should never be a factor when authoring code.
* code beautification is also highly subjective. Everybody has different opinions on beauty and easy to read. Providing the means to allow developers to exercise those opinions dynamically and automatically makes people happy. So long as it’s just vanity the code stills executed and reads the same way.
Distributing source code with significant white space seems like a totally solved problem to me. When has this come up for you?
> Line termination differs by OS. Now it’s largely just posix vs Windows, but there used to be even more variance and there could be more in the future.
Why would there by more types of line terminators in the future? We already have two ways to do it, which is one too many. Who would add another? And if they did, why would all the text editors and software out there in the world be expected to adopt it?
> * code beautification is also highly subjective. Everybody has different opinions on beauty and easy to read. Providing the means to allow developers to exercise those opinions dynamically and automatically makes people happy. So long as it’s just vanity the code stills executed and reads the same way.
Personally, I don't care about people's subjective opinions or what makes them happy. I just want to be able to easily understand what the code does. I find it easier to understand code when it is formatted in a way that I'm familiar with. If each programmer has their own little formatting idiolect, that makes it harder for me to read their code. So, I think it's great when standardized formatting is built into languages, either with formatters (like go and elixir have) or by having a syntax that enforces uniform formatting (as is the case here).
I read that as only caring that people are forced to do what makes you happy. To me that sounds extremely selfish, perhaps psychotic. Different things make different people happy. I try to be objective about that.
And, oh yea, consistent formatting between projects makes me happy. Different people have different preferences and they can’t all be accommodated.
Not sure how me advocating the trade off of consistent formatting over allowing formatting idiolects makes me “psychotic.”
No. Formatters have won. All code should be run through a formatter that contains zero options. The result is the how the code looks. Obviously languages with significant whitespace are more limited in what their formatters can do, but the point about it being subjective is irrelevant.
Why must the formatter be opinionated? Why should a single form of vanity be forced on users? If my software were so opinionated nobody would use it.
In Muon, you can always add more functions to an existing namespace, so if you wanted to have a function with that behavior you could add it.
Does it mean C is a data orientated language?
1. "Data oriented. Just functions, structs and enums. NO: classes, inheritance, properties, etc."
I think you should allow for struct "inheritance", at least in an Oberon-style manner. There are many cases where having a common "head" between structs is useful (e.g. widgets for a GUI toolkit, object types for a scripting language, etc).
2. "Memory is initialized to zero. Array bounds are checked (can be turned off where needed)."
It is not clear from this, but how can both be disabled in a case-by-case basis? For example in D local variables are initialized to zero by default but you can do something like (IIRC): Foo foo = ? (or = nil, i do not remember) which the compiler interprets as "do not zero-initialize this). Similarly can i do something like array[foo] (bound checked) and array{foo} (non-bounds checked, imaginary syntax)?
3. "An LLVM backend is in the works which will avoid any undefined behavior."
LLVM inherits most of C's undefined behavior, so it will not solve your problem there - you'll need to explicitly generate code that works around undefined behavior. This is a problem that the Free Pascal developers also faced (the language has much less undefined behavior than what LLVM assumes).
4. I am not fan of the language enforcing code style (e.g. "The name of a type parameter must be a single uppercase character." and significant whitespace)
5. Nitpick, but "List<T> struct #RefType { dataPtr pointer count int capacity int }" is a dynamic array, not a list (also, an example of why significant whitespace isn't a great idea :-P)
6. "Type inference for function return values and locals"
I actually prefer to explicitly see the data types passed around, it helps better understand what exactly the code is doing (not just the algorithms, but also the data types which are important) without needing to have too much "implied code" in my mind (i.e. with explicit data types i don't need to "know" that "foo" returns "bar" when looking at its use in a diff (or any other non-syntax highlighted, non-code aware code viewing context), i actually know it because i see it in the code).
(FWIW for this reason i also dislike auto in C++ and similar constructs in other languages - you save a bit of short term typing now, for a lot of long term headache later)
7. "fgets(s pointer #As("char "), size int, stream pointer #As("FILE ")) pointer #Foreign("fgets")"
I think you need to make your type system good enough to natively express C types, otherwise the string-based pseudotypes will bite you hard later when you want to move away from using C as a backend.
8. I do not see any mention of a preprocessor, how is code supposed to IFDEF stuff (like, e.g. a commercial program that needs to have something like IFDEF DEMO, or a source file that needs to behave slightly differently if linked as a static library or dynamic library, or the use of two APIs like e.g. IFDEF LINUX, IFDEF WIN32, IFDEF OPENGL, IFDEF VULKAN, etc) or work around language limitations (like the non-availability of inherited structs mentioned above that C programs often work around by using a macro that defines common elements for structs)?
9. "Number literal suffixes"
I'm not fan of the underscore in the suffixes and also i'd avoid the uppercase L and introduce an i (that is 'nop' but could be useful for automatically generated code via scripts and/or macros).
There are other minor nitpicks, but i'd need to actually check it in practice to write more :-P.
1. Right now, you can emulate this with the transmute builtin function. Not yet sure if the additional complexity of this feature would be worth it.
2. Array bounds checks can be avoided by using the unchecked_index builtin. Uninitialized locals are not yet possible, but I think it makes sense to eventually add a construct to allow that, for situations that need every last bit of performance.
3. I've only started to look into LLVM recently, thanks for the heads up.
4. See my comment here: https://news.ycombinator.com/item?id=19599908
5. C# programmers would like to disagree with you ;). Note that the formatting issue is not due to significant whitespace, as you'd have the same issue writing up a Golang snippet.
6. Editor support (specifically, language server) should help a lot here.
7. The string based types are only used for inserting the right cast to make the C compiler happy, so they should not be an obstacle.
8. Compile time evaluation can be used for some cases. Adding a preprocessor comes with its own set of tradeoffs, haven't yet decided what to do there.
9. A lowercase l looks a lot like a 1 in many fonts, hence the uppercase L. Will consider adding an i suffix.
5. I also tend to name dynarrays as lists, but in this case you have both List and Array and the only difference seems to be that one has a capacity while the other doesn't. Having both can be a bit confusing when you actually wanted a (linked) list.
6. It will only help when inside the editor, hence why i explicitly wrote "when looking at its use in a diff (or any other non-syntax highlighted, non-code aware code viewing context)".
8. None of the examples i mentioned can be solved with compile-time evaluation (except some cases of IFDEF DEMO) since they change what code is actually compiled (e.g. if you want to use an API that may not even be available on a target - like e.g. DirectX on Linux) or generate code themselves.
9. I do not think it makes much sense to worry about that, there are fonts that have 0 and O look the same, let the user fix their fonts, do not punish those who use unambiguous fonts with inconsistency. Trust your users.
6. Sorry, I totally missed that part of your comment! A language server won't help in that case. At least wrt to return types though, you're always allowed to specify them for clarity. I can imagine building some tooling that automatically infers and adds explicit return type annotations if they are missing. You could run this as a commit hook if you wanted to enforce that as a coding standard.
8. Right, I was referring to the IFDEF DEMO case (assuming it's used to implement straightforward feature flags). I agree that for the other cases that you mentioned we'd need something else. For now, for some situations, you could compile with different files (e.g. gfx_opengl.mu, gfx_directx.mu, etc.), though it's a pretty crude solution, especially if you want to combine conditions (e.g. Linux + OpenGL). Definitely thinking about this.
Long term, I agree that a package/dependency manager would be great! There's already so much work to do though, so this will unfortunately be out of scope for the near future.
https://github.com/nickmqb/muon/issues/2
Your roadmap looks great as well. Have you considered any of the following?:
- Lisp-style Macros (e.g. Macros as functions that transform an AST object) -- this would also answer many of the preprocessor questions you had, especially if you can specify compile-time behavior (like in zig)
- Github-based decentralized package management system
- Owned pointers memory management options
I'm considering adding some meta-programming/reflection capabilities in some form, if they don't increase the complexity of the language too much. Lisp-style macros would be a possible approach, compiler plugins would be another. The latter approach might be easier to implement though.
A package manager will be out of scope for at least the near future, but long term it would be great to have one!
Re: owned pointers. I'd recommend that people look into more coarse grained memory management strategies first (e.g. arena allocation), and use one of those if possible. However, these strategies definitely have their limitations, which is where alternatives like owned/shared pointers could come in. However, to implement a "proper" owned pointer, e.g. an alternative to std::unique_ptr<T>, we would need destructors (which Muon will never implement) and move semantics (of which there is a chance that Muon _might_ implement it, but probably not anytime soon). With move semantics we can perhaps create a lighter weight owned pointer that is still useful; I'll have to experiment a bit to see how that would work out.
In the Go community they have found returning the result/error tuple is tedious enough that people don't properly handle their errors. I would argue this is because of the lack of generics and computation expressions.
In F# I can do something like this:
This lets you create little DSLs and code that you can handle errors in a central location and in your main code just get the work done without thinking about it. Makes for a really great workflow.See also https://github.com/louthy/language-ext