Ask HN: If you designed a new programming language, what would its features be?
Funny you ask that, because I <i>am</i> making a programming language!<p>(1) Structured concurrency, best explained at [1], [2], [3], and [4]. More on this later.<p>(2) Extensibility. Most people think that this means macros. It doesn&#x27;t. However it&#x27;s done, as curryst said, languages are not perfect, and extending a language is important. See &quot;Growing a Language&quot; by Guy Steele, Jr. [5] More on this later.<p>(3) Like many other posts, having real-world units like feet, meters, pounds, kilograms, etc., for calculations and type checking, but the default number type should also be a BigRational.<p>(4) Ranged types, like Ada, allowing programmers to explicitly limit the range a type can be, eliminating errors. For example, a type that is used in the denominator of a division could be restricted to being not zero, and you don&#x27;t need to worry about divide-by-zero for those divisions.<p>(5) Consistency, a <i>massively</i> underrated feature. The use of something in the language should be the same <i>everywhere</i>. This simplifies learning and makes it easier to understand features you haven&#x27;t seen yet.<p>(6) Design-by-Contract and other formal methods &quot;in the small&quot;. It should be possible to use formal methods on little pieces of code. Even if you can&#x27;t go so far as to prove them correct, it should be easy to increase your <i>confidence</i> that they are.<p>(7) Conditions and restarts instead of exceptions. Conditions and restarts come from Lisp, and it is one of the greatest ideas in computer science that almost nobody knows about.<p>(8) Composition, not inheritance. I think that most programmers have agreed on this.<p>(9) A way to solve the Expression Problem. This can be multi-methods or some other way.<p>(10) Dependent types. This will help with formal methods, but they are also powerful outside of that context.<p>There&#x27;s definitely more, but I&#x27;m starting to feel lazy.<p>About (2), as implied, it is possible to make a language extensible without macros. This is done by fully defining the internals of the compiler and allowing the language access to them. In fact, you get 99% of the way by not even defining <i>everything</i>, but by just defining the IR and allowing the programmers to add keywords with functions to parse after them. In fact, I would argue that defining keywords gets you 100% of the way that macros do because macros are, in essence, keywords themselves.<p>As an example, say a programmer builds a build system in my language and call it &quot;awesomebuild&quot;. They can define the keyword &quot;target&quot; for making a target to build, and others can use it, like so:<p><pre><code>    import awesomebuild

    awesomebuild.target program_name: prereq1, prereq2 {
        &#x2F;&#x2F; `$` Means run the following as a command.
        $ compiler -o program_name prereq1 prereq2
    }
</code></pre>
About (1), I will just reproduce a comment I made on lobste.rs [6] here.<p>Structured concurrency will be, I think, amazing, but I have had an idea recently that I think would make it even better.<p>Structured concurrency, as currently defined, turns execution into a tree of threads. But what if we could turn it into a DAG?<p>It would have these restrictions:<p>(1) To prevent potential memory cycles (and thus, needing a garbage collector or reference counting), forbid all types from being able to, directly or indirectly, have pointers to themselves.<p>(2) Some form of RAII with automatic calling of destructors, like C++ or Rust’s Drop trait.<p>(3) Have structured concurrency as it currently stands.<p>(4) But add the ability for threads to ask another thread to run code for them.<p>Number 4 is the magic. What happens is that, when a thread creates data (the “create thread”) that other threads might want to use, it then creates a child thread to run the code it would have run on the data and waits for requests from other threads. Then, when other threads put in requests for the data, instead of getting a pointer to that data and continuing on their merry way, requiring reference counting for the data, each thread sends a function to the create thread, from which the create thread spawns another child thread that has access to the data it created, which would give every thread indirect access to the data that it wanted.<p>Then, every thread that requested the data is paused, waiting, while the execution of the thread it requested runs.<p>The reason the create thread spawns a child thread instead of just running code on the data itself is two-fold:<p>* It allows it to respond to requests, and<p>* It prevents the situation where an already existing child of the create thread requests the data and then the child is dependent on its parent to exit first. If the child of the create thread is dependent on a sibling to exit first, all is well.<p>In other words, creating the child thread first is what keeps the execution a DAG.<p>Restriction number 1 is important to get rid of all need of any kind of GC, including borrow checking!<p>Some may claim that it’s impossible to eliminate cycles, but you can always create a “parent” type that points to all of the child types that could be in a cycle.<p>For example, to create a linked list, create a parent type that stores an array with data, and alongside the data, it stores handles that it knows how to interpret as the prev and next pointers. The children don’t know how to interpret those handles, but the parent does, and it can do everything you need it to do in order to emulate a linked list.<p>A programming language that provides these facilities should also have reference counting, like Rust does despite its borrow checker, because sometimes, more complicated things are needed. However, if a programmer decides to program with these restrictions, they would have these benefits:<p>* No memory leaks.<p>* No use-after-free.<p>* No memory safety issues.<p>* No borrow checker to explode compilation times.<p>* Memory allocation that could be entirely done with bump allocators, except for data that may need to grow, such as resizable arrays or maps. Even dynamically-allocated stuff that knows what size it needs, but doesn’t need to change its size after allocation, could use the bump allocator.<p>While there are some details (a compiler would need to figure out when an item escapes a function), I think these ideas could be pretty useful.<p>As for escape analysis, we could make it easy and say that the compiler should just error if a value escapes. I personally would prefer something more sophisticated, where we ensure that a value does not escape to global variables. If a value escapes by being returned, the compiler should just treat that as though the caller has responsibility for it, and it already knows what to do in those cases (nothing in the callee, and call the destructor in the caller, unless it escapes, etc.).<p>[1]: <a href="https:&#x2F;&#x2F;250bpm.com&#x2F;blog:71&#x2F;" rel="nofollow">https:&#x2F;&#x2F;250bpm.com&#x2F;blog:71&#x2F;</a><p>[2]: <a href="https:&#x2F;&#x2F;250bpm.com&#x2F;blog:137&#x2F;" rel="nofollow">https:&#x2F;&#x2F;250bpm.com&#x2F;blog:137&#x2F;</a><p>[3]: <a href="https:&#x2F;&#x2F;vorpus.org&#x2F;blog&#x2F;notes-on-structured-concurrency-or-go-statement-considered-harmful&#x2F;" rel="nofollow">https:&#x2F;&#x2F;vorpus.org&#x2F;blog&#x2F;notes-on-structured-concurrency-or-g...</a><p>[4]: <a href="https:&#x2F;&#x2F;gavinhoward.com&#x2F;2019&#x2F;12&#x2F;structured-concurrency-definition&#x2F;" rel="nofollow">https:&#x2F;&#x2F;gavinhoward.com&#x2F;2019&#x2F;12&#x2F;structured-concurrency-defin...</a><p>[5]: <a href="https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=lw6TaiXzHAE" rel="nofollow">https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=lw6TaiXzHAE</a><p>[6]: <a href="https:&#x2F;&#x2F;lobste.rs&#x2F;s&#x2F;8msejg&#x2F;notes_on_structured_concurrency_go#c_wkxbvb" rel="nofollow">https:&#x2F;&#x2F;lobste.rs&#x2F;s&#x2F;8msejg&#x2F;notes_on_structured_concurrency_g...</a>