The Neat Programming Language
neat-lang.github.io
neat-lang.github.io
I think this is a bit off. It is an issue with Java and C#, but they don't position themselves as a C++ replacement in the same way D does. Even so you still get lots of complaints about missing RAII and GC pauses (witness how well received Go's low latency pauses have been received).
And yes glibc could introduce arbitrary delays, but it generally doesn't. Allocation is way faster than GC.
Maybe he was talking about long pauses when you destroy a big C++ object (e.g. a big `std::map`)? That can definitely cause annoying big pauses due to deallocation. But he already identified the critical factor - it's predictable. You can fix it deterministically.
Anyway reference counting is a decent choice. It can be very fast (especially if you are only referencing counting big objects and not little integers etc.)
Yeah it does, that's why gamedevs and hfts don't use any malloc in the fast path, glibc or otherwise.
* Not allocating (or stack/bump allocation): extremely fast, completely deterministic.
* malloc: pretty fast, in theory arbitrarily slow but in practice it's well bounded
* GC pauses: very slow (until Go anyway)
With GC you can try to avoid allocations, but you might miss a couple and get occasional frame stutters anyway every now and then. Also avoiding allocation is a whole lot harder in languages that use a GC for everything than languages that provide an alternative.
The real solution for games is explicit control over GC runs - tell it not to GC under any circumstances until the frame is finished rendering, then it can GC until the next frame starts. I assume Unity does this for example. Still, games are only one application where you don't want big pauses - one that happens to have convenient regular times when you probably aren't doing anything.
> With GC you can try to avoid allocations, but you might miss a couple and get occasional frame stutters anyway every now and then. Also avoiding allocation is a whole lot harder in languages that use a GC for everything than languages that provide an alternative
Something that I think doesn't get the attention it deserves in Rust is how explicit it makes allocations at the type level. You don't have a pointer that maybe points to the heap and maybe to the stack (or in the case of GC maybe even is on the stack right now but won't be when you change how it's used later) or a slice that you need to track down whether it originated as a reference to a fixed-size array or if it it's dynamically allocated; you have either type that you know is a reference like &T or &str or a slice, or you have a type you know was allocated on the heap like a Box or an Arc or a String or a Vec. I've seen people need to spend a lot of time profiling projects in other languages to track down where they can optimize their memory usage. I wonder if people who are skeptical of languages with expressive type systems might see the benefits more if they were presented less in terms of what the type system provides you directly, but in terms of what it makes available for tooling to take advantage of; in the case of using the type system to track allocations, the advantage might seem limited to your own code and not helpful when handling dependencies without being willing to dive into their code, but I think it's easy to overlook that having the information available statically makes it possible for tooling to utilize it, and that applies just as much to dependencies.
- As you described, you can disable the GC to do your thing. That's not the solution I would recommend, but if you call GC.disable then the GC won't collect anything until you enable it back (or if the program is running out of memory).
- You can mark a function as @nogc, and then the compiler will prevent you from allocating anything that could trigger GC allocation.
There is some level of support for @nogc in the language and few libraries to help, the issue is more in the standard library which relies a lot on the GC.
If by "slow" you mean "high latency", then this isn't true. Plenty of GC algorithms provide guaranteed sub-millisecond pauses.
It's still not the norm, but the point is that those were available
> and "sub milliseconds" is not a high bar. Malloc is much much faster than that.
Malloc is not guaranteed to return in that time frame. The sub-millisecond latency of these GCs is a real-time guarantee.
No but in practice it always does.
I can tell I'm gonna enjoy Neat. The dev understands perfectly what I like and dislike in a language.
It keeps getting better
My DConf talk on the language is now up too! https://www.youtube.com/watch?v=nDqlYnS-K2c
But the mere fact that the primary ascribed goal is "fun" suggests it is not be taken seriously for actual work projects.
(Though I will be proper chuffed if you do. I just can not recommend it.)
To be honest, my goal is to have maybe 10 to 50 users right now. That'd be quite enough for this christmas.
- proper macros
- proper sumtypes with implicit conversion
- more normal lambdas (seriously, D's lambdas are wild)
- faster compiler with more caching and (maybe) eventually live reload?
- proper packages instead of include path.
And a bunch of small fry like format strings and named parameters.
And yeah, um, GC can be good but the D GC kind of isn't. We use D at work, and we run into GC issues frequently. Neat's RC is a much thinner wrapper around C memory allocation, which is just better imo, much as GC and RC are logically equivalent in some ways.
int factor = 2;
assert([2, 3].map!(a => a * factor).array == [4, 6]);
Then the `!` indicates that you're actually passing the lambda as a compiletime parameter to `map`. But the lambda can access the surrounding context! How does it pass a runtime value at compile time?So what you're passing is actually purely a function symbol. The way that it gets the stack reference to the surrounding function is that it's actually a nested function. And the way that `map` gets the stack reference to pass to the lambda is that, effectively, that instance of `map` is also a nested function of the calling function.
That's also why you cannot pass a lambda as a template parameter to a class method in D: it already has a context parameter, ie. the class reference.
In Neat, the value of the lambda is the stackframe reference, and it's just passed as a regular parameter:
int factor = 2;
assert([2, 3].map(a => a * factor).array == [4, 6]);
Which avoids this whole issue at the cost of requiring some cleverness with refcounting.It also encourages people not to think about what the structure of their templates, so you can end up with truly massive amounts of duplication.
Please elaborate on this. It's something I'm trying to get right in my own language since day one. I see in your language's manual that you have chosen to implement modules as files and packages as directories. I came up with something very similar but special cased the main module file so that the entire module could be contained in its directory.
my-module/
my-module.ln
(import (my-module)); reads my-module/my-module.ln
How do you represent modules in your implementation? How do you handle module loading? What paths do you search? Do you support Linux distribution packaging? This last feature is something I'm interested in supporting, I added system directories to my search path for this reason but I wonder if there's anything else that I need to do.So Neat's actual hierarchy is "file [> folder]* > package", where `package` itself is optional in the import declaration: you can write `import package(gtk).gtk`, but you don't have to. This is occasionally useful during bootstrap builds when you want to clarify which version of the compiler an import is coming from: the current running compiler is always `package(compiler)`.
This is all because I've been writing it with something like a package manager in mind from essentially day one.
edit: I'm not looking at distribution packaging right now, because I'm not looking at non-source libs at all. That's something that can come later if it's needed at all.
It would be indeed quite neat (pun intended) to see D adopt them as well, specially sumtypes, Rust went ahead and made enums better, it's quite an expected feature for a language at this point
So much good has come of it...
Are the changes to the language going to make it much better? Break existing code? How long until they finish that, is it nearing completion or just beginning now?? So many questions as someone new to D.
The problem the OP was talking about is that in D, you cannot implicitly use an `int`, say, where `SumType!(int, string)` is expected.
You need something like this:
alias StrOrInt = SumType!(int, string)
void takeStrOrInt(StrOrInt s)
{
writeln(s);
}
StrOrInt value;
value = 10; // ok
value = "foo"; // ok
takeStrOrInt(value); // ok
//takeStrOrInt(10); // not ok
//takeStrOrInt("foo"); // not ok
takeStrOrInt(StrOrInt(10)); // ok
takeStrOrInt(StrOrInt("foo")); // ok
Even though this is not perfect, it works quite well (I believe it's zero cost to do `StrOrInt(10)` for example, but I'm a D newbie).
It's a bit crazy for me to see people creating new languages instead of help improving existing ones because of minor stuff like this. The effort to create a language and a stdlib and a package manager etc. is ridiculously high compared to improving existing languages. publish(TrackingEvent(UpdateTrackerEvent(trackerId, position)));
that just adds visual noise. And the fact that you cannot return out of sumtype apply expressions just makes so many neat idioms impossible. Something like Object obj = nullableObj.case(null: return false);
is just fundamentally impossible in D.So it's not one thing, it's a lot of things coming together. :) Mostly I just realized one day that D was never going to be the perfect language for me, because it wasn't even interested in being that language.
> The effort to create a language and a stdlib and a package manager etc. is ridiculously high compared to improving existing languages.
Have you seen the DMD source code? Genuinely, writing my own compiler was easier than improving DMD.
As a funny side note, I read the part on your website where you talk about why you chose reference counting over garbage collection, and despite your light-hearted resentment of the fact that garbage collection is such a turn off for people, the fact that your language doesn't use garbage collection is one of the major factors in why she might end up using it lol. We decided not to do C# (my first suggestion actually) because since she doesn't want to use an engine, it wouldn't be used just as a scripting language, but for the entire object model and update loop and so on, so GC would be a dealbreaker. Sorry-
(tiny rant mode as a hobbyist gamedev myself) there is actually a good reason for this -- it is much easier to do multithreading when you don't have a separate thread going over all shared memory, and it's much easier to do a soft real time thing like game development if your memory allocation and deallocation stays relatively consistent and predictable, even if it is slower overall. Also, yes malloc may cause variable slowdowns too, but definitely to a smaller degree/varience than the average garbage collector and more predictably since you can control when allocations happen; otherwise there wouldn't be a noticeable difference between using garbage collected and non garbage collected languages for writing large-scale games. Plus, in any case, that's the reason why game developers tend to try to limit dynamic allocation at runtime in the first place.
Not yet, but I'm sure she'll be overjoyed to! She's so excited about this language, it'll finally make low level small scale gamedev accessible to her! :D
> Anyway, please also tell her to hit up the Discord (or IRC) for any questions!
I will! I'm sure she'll be excited there's an IRC
He doesn't have much time for the agents of corporations who through their foundations and charities aim to influence and seize control of such groups with their demands for CoCs, DEI in return for some funding and their choice of board appointees.
Nim has been getting along fine without them and will continue to.
I didn't notice any mention of tail calls. Does it support proper tail calls?
And perhaps declare some special variables as "resource" to inform the compiler to really enforce the single write owner rule, for "special" external things like file descriptors etc.
IMHO, memory as a tightly managed resource is adequate for system programming, but a hindrance in most other general programming tasks. But the approach has its point for actual resources any program needs to manage, allowing powerful compile time checks.
So make it optional, with the default being automatic memory management.
You can define types in Neat that are noncopyable, but it really limits what you can do with them. For instance, nested function references are noncopyable by default to avoid forcing closure allocations.
"Compile time reference counting / lifetime analysis / borrow checker."[1]
"Reference Counting with cycle detection at exit, 95% of reference count ops removed at compile time thanks to lifetime analysis."[1]
This is probably the least useful of all comments, but please think of another name. My quibble with the language has to do with the use of '[' .. ']' pairs. I'm not confident that refactoring will be straight-forward. I could be wrong.
Not sure what you mean with `[]`?
looks problematic to me -- are the brackets indicating scope? an array? something else?
As far as the naming is concerned, you'll probably have to put up with remarks such as "neat code is messy" because that's the way people are with something new. Don't let that dishearten you!
Breakelse: When Compiler Developers Get Bored - https://news.ycombinator.com/item?id=37887426 - Oct 2023 (78 comments)
Neat as in the current iteration of the language, started in 2020.
I guess it’s more accurate to say that you’ve been an inveterate language designer for a very long time, and it’s nice to see your latest one go public.
macro import std.macro.cimport;
import c_header("raylib.h");
And then you can just use (most of) the Raylib functions and types.cimport is a massive hack: it runs gcc on the header in preprocessor expansion mode, then parses the result. Its tactic for C syntax it doesn't understand is "just skip it and hope for the best." :) Works surprisingly well.
On x86, there was a lot to be gained from having a language specific ABI, because the cdecl ABI was so slow. But the x86-64 ABI, much as I may dislike its complexity, is genuinely plenty fast already.
Well, the names (which I enjoy) are similar anyway, that's what made me think of the no doubt overstated analogy.
- LLVM is a proper SSA backend, it just uses the llvm-c API to generate bc modules, like any other LLVM-based compiler. It technically links with clang, but that's just cause I didn't want to set up my own link driver and optimization pipeline.
- GCC is technically "transpiling" to C, but the generated C files are unreadable, because it's still SSA.
The GCC backend is also used to build the releases. I just build the compiler with the GCC backend, then zip up all the .c files that it produced.
That's why the release build script starts by building a bunch of C files.