It is mostly for fun and exercise in language design, I don't have any grand aspirations for it. It is however, by this time, a usable general-purpose language.
Alumina borrows (zing) heavily from Rust, except for its raison d'être (memory safety). Syntax is a blatant rip-off of Rust, but so is the standard library scope and structure.
Alumina bootstrap compiler currently compiles to ugly C, but a self-hosted compiler is early stages that will target LLVM as backend.
If that sounds interesting, give it a try. I appreciate any feedback!
Standard library documentation: https://docs.alumina-lang.net/
Online compiler playground: https://play.alumina-lang.net/
Well-established languages have tons of widely used, highly vetted libraries implementing functionality that doesn’t exist in the new language and would be totally impractical for an individual to implement themselves. For example, if I’m doing scientific computing, I need a good linear algebra library like Eigen or Armadillo (or Numpy/PyTorch/Tensorflow), all of which would be impossible for me to implement myself.
Therefore, for a new language to catch on, it needs a good foreign function interface. Yet FFIs are almost never brought up whenever new languages are discussed on HN. (Again, I realize that this is just a fun project and widespread adoption is not a goal.)
[1]: https://zenodo.org/record/4711425
[2]: The paper should be up in a few days I suppose.
Check for example the language bindings to LLVM's C API (fairly low level) and Tree-Sitter which is used internally (a bit higher level bindings)
https://github.com/tibordp/alumina/tree/master/libraries/llv...
https://github.com/tibordp/alumina/blob/master/libraries/tre...
I think UFCS makes it quite nice for bindings, since external C functions can be used as if they were methods if the object is passed as the first parameter. So in many cases there might not even be a need to write wrapper structs for bindings that feel native.
Of course, it's still a manual process and since Alumina is just a compiler and stdlib for now (no llibrary ecosystem, no compiler driver), it's a bit cumbersome. But I like the approach Rust has with bindgen and cc crates, to automatically create bindings for C and C++ code.
Nim, Zig and Rust do too, but they have semantics much closer to C, so it’s almost free (especially when they can all use llvm directly for code generation, and Nim’s preferred way is to compile through C in the first place)
import stdio;
void main() { printf("hello from D!\n"); }
is all that's necessary, as D has a built-in C compiler that reads stdio.h, compiles it, and presents its interface to the D code.In retrospect, it made sense given that in my community we make our own furniture, instruments, tools, foods from whole items. Code seemed like the next logical step. During travels, once there was a man who had a word in his language for this. He said it was an aphorism loosely translatable to "the beauty of struggle". As he explained it, this is the positive benefit gained in return for the time and effort to do something as an act of appreciation of the craft, and how the value in the experience surpasses the debt of time and sweat.
To put it in a more modern and eastern philosphical context, think of it like Kata.
Do you really intend to write your own library for a given task when there is a perfectly good and mature library that does that task and more freely available?
How long would it take for you to write your own Sqlite or your own Nginx?
Also, wonder how long it'll take until we see a "Aluminia" fork...
It's full of unnecessary noise and additionally very irregular. (With complete craziness thrown in between like the semicolon rule to "visually distinguish" procedures and functions, which must be a kind of joke I don't get).
I really don't understand how such a conceptionally well thought out language got this pretty ugly syntax.
(And no, you don't need such ugly syntax "because language features". Just have a look at Scala 3 that is much more powerful but maintains a clean, almost pythonic syntax).
Rust is in part designed to look familiar to C programmers. Rob Pike once famously described C syntax as able to survive a channel that mangled whitespace. That's less of an issue now, but habits die hard. Indentation in place of syntax is nevertheless controversial. That's a toy version of the bigger question: Why should text be required to carry a load that a smart editor can infer? In Clojure, some prefer ;; to ; because the comments stand out better. Um, syntax coloring? I dislike all comment characters, and for many years I preprocessed a practical version of "comments are flush, code is indented".
Ownership is nevertheless pure genius, and one can define more powerful operations on Rust's restricted notion of data than on general data. I'm imagining a "lisp without parentheses" that transpiles monadic parsers on steroids (aimed exactly at Rust data) to Rust. It will take me a while.
(though in Rust's case I think it's more accurate to say it distinguishes between "statements and expressions" than "procedures and functions")
Using "<" and ">" as both operators and delimiters
Turbofish
Symbols instead of words (ref -> &, and -> &&, not -> !, …)
Inconsistency (Why [i64; 5] and not something like array<i64, 5>?)
From what I gather, it might be more accurate to say that Alumina has no ownership model. Rust requires manual memory management, but offers the ownership model as a compile-time tool for doing so.
It would actually be pretty interesting to see some experimentation around alternative ownership models.
IMHO the way it’s used in Go is a workaround, of luck of destructors, not a feature.
Edit: not a criticism on your language OP, which is better than what I could have ever built. Just a comment in the “defer” trend.
I think one case where defer might be nicer is for things that are not strictly memory, e.g. inserting some element into a container and removing it after the function finishes (or setting a flag and restoring it).
This can be done with a guard object in RAII languages, but it's a bit unintuitive. Defer makes it very clear what is going on.
Some syntactic sugar, like Python’s “with” should help with that, shouldn’t it?
What I meant was something like this (could also be done with `contextlib`, but it's also verbose)
seen_names = {}
class EnsureUnique:
def __init__(self, name: str):
self.name = name
def __enter__(self):
if self.name in seen_names:
raise ValueError(f"Duplicate name: {self.name}")
seen_names.add(self.name)
def __exit__(self, exc_type, exc_value, traceback):
seen_names.remove(self.name)
def bar():
with EnsureUnique("foo"):
do_something()
...
With defer this could be simplified to static seen_names: HashSet<&[u8]> = HashSet::new();
fn bar() {
if !seen_names.insert("foo") {
panic!("Duplicate name: foo")
}
defer seen_names.remove("foo");
do_something();
} let stream: &dyn Writable<Self> = if output_filename.is_some() {
let file = File::create(output_filename.unwrap())?
defer file.close();
file
} else {
&StdioStream::stdout()
};Basically, it is going to be a full-featured Compiler front-end foundation library with incremental parsing capabilities, error recovering, AST manipulations, etc, but written entirely in Rust, and hopefully with more user-friendly API for Rust devs.
May I ask you to give me some feedback on your experience with Tree Sitter, and the challenges you faced during the development of your compiler's front-end?
Thanks in advance!
The only two pain point I had is that the `node-types.json` that's generated only contains the names of the nodes, not the numerical IDs. This means that if you have some codegen generating Rust enums is difficult if you want to avoid matching nodes by string.
I wrote https://github.com/tibordp/tree-sitter-visitor for generating visitor traits in Rust for a given grammar. I actually did it a bit differently in the end for Alumina, but it might come useful.
How difficult was it for you to design a whole programming language?
Do you have a theoretical CS background?
If I want to design my own, would learning Racket and other LISPs help?
I'm interested in formal methods and embedded systems.
Protocols were probably the trickiest feature of the language to figure out. As for the compiler itself, surprisingly, the biggest hurdle to get over was the name resolution. It's a tiny part of the compiler today, but everything else was much more straightforward.
I don't have formal CS background, but I have been coding for a long time. I read the Dragon Book and would recommend it to anyone writing a compiler, even though it's a bit dated.
I don't know Racket or LISP myself so I cannot comment on that part.
The sandbox is running the code server-side in a nsjail container.
As for unwrap, I feel you! the try expression (expr?) is supported, which makes it look a bit nicer, but I'm still trying to figure out a good idiom for when you actually want to do specific things based on whether the result is ok or err.
Alumina does not have Rust-style enums (tagged unions) or the match construct, which makes it a bit tricky.
What if Rust provide a separate analyzer to analyze potential memory leak from this language.
The only potential problem I see that with the current C backend, the debugging information is very hard to trace back to the original Alumina source code, so it might be hard to see where the leaks are coming from. This is something I plan to address in the self-hosted compiler, once it is functional.
Were there similarities besides "syntax inspired by Rust"?
The overarching theme is to see how far you can go making a language that feels high level without having a garbage collector or RAII. I used to use Deplhi/Pascal a lot when I was younger and it was this kind of language.