Bubbleos, a Self-Contained OS
gitlab.com
gitlab.com
> Leconscrip: a memory-safe low-level statically-typed imperative language without garbage collection and with Lisp syntax, […]
I think producing anything even vaguely useful of this description in under a month was fairly wildly unrealistic. The only path to memory safety without garbage collection (… unless you’re willing to forego references altogether, which would generally disqualify usefulness) is some form of ownership tracking, and that’s a fairly lightly-trodden and lightly-documented path. There are very few examples of such languages even if you skip Lispness—I think Cyclone was the first (a research project spanning 2001–2006; and correct me if there was prior art), and Rust is the only even vaguely mainstream one. Making a language with these features using Lisp syntax (which I presume to include macros, otherwise is it even Lisp syntax?) is even more lightly-trodden and I suspect more difficult on average, though Carp <https://github.com/carp-lang/carp> looks to be having a go at that.
(I’m definitely interested in the concept of a memory-safe, garbage-collection-free language with Lisp syntax. I wish Carp and any other attempts well.)
My plan was, indeed, to forgo references almost entirely. https://dercuano.github.io/notes/leconscrip.html describes some of my thoughts on this from 02018. Forgoing references entirely sounds extreme, but keep in mind that VHDL, Verilog, Tcl, FORTRAN 77, APL (pre-APL2), BASIC-80, GW-BASIC, and even QBasic all have this same limitation, and many people would describe those languages as "even vaguely useful". (All of them are memory-safe but only VHDL, FORTRAN, and the BASICs are statically typed.) The objective for Leconscrip was not to be a good language to write a whole system in, but to be a good language to write the lowest-level userland parts of the system in, like a terminal emulator; in particular, parts that need to provide compile-time guarantees on worst-case execution time, memory usage, and not failing. (However, note that in the Lecon form described there, we have arrays which can be indexed by variables, which requires run-time bounds checking and thus the possibility of failure.)
As you can see, my first thought was to use JS syntax, but later I thought that if I just used Lisp syntax, I could avoid wasting any time on parsing. Macros weren't particularly interesting to me, since I expected to be the only person programming in Leconscrip, so there's no particular advantage to being able to add new syntax to Leconscrip without modifying the compiler; I could modify the compiler as easily as I could modify whatever program I was writing in Leconscrip.
In Lecon my plan was to provide for passing arrays as parameters, as in FORTRAN, which provides a limited form of "references", in that you can invoke a subroutine with an array parameter and have it mutate the contents of the array.
Perhaps if I describe Lecon as "a low-level FORTRAN-like language with Lisp syntax" it sounds less wildly unrealistic to hack together in under a month! It's still probably more than a day, though.
However, that's just "Leconscrip level 0: Lecon." I think I can do better than that. Records (structs) can provide for aggregate data types that don't require constant bounds-checking the way arrays do. Pascal-style "var parameters", in which the callee's parameter is an alias for an lvalue in the caller, permit threading a reference to a record through a call tree, allowing the record to act as a state machine, but without an additional referencing mechanism, preserve memory safety without garbage collection. (You could think of this as a very primitive form of Rust-style ownership tracking.) And in fact original Pascal also supported procedure arguments, which carried an implicit reference to the stack frame where they were created, and were subject, in essence, to the same limitations as var parameters. This permitted higher-order programming in Pascal, "using closures," without garbage collection; but it was kneecapped by Wirth's clumsy syntax, which, like Python, had no room for Ruby-style block parameters. This, too, requires no groundbreaking research.
I think yielding to a block parameter can be made considerably less expensive than a procedure call (by preserving some call-preserved registers into the resumption of the caller's context in the block), which would enable you to do higher-order programming in Leconscrip at a considerably lower runtime cost. You just don't get upward funargs is all. But Lisp didn't have upward funargs until the 01970s anyway.
Pattern-matching on ML-style discriminated unions (sum types) offers the potential for, among other things, better failure handling and GUI event handling, though without references they're fairly limited.
More notes on this are in https://dercuano.github.io/notes/imgui-programming-language.....
A more difficult problem is how to handle iterative data structures. ML does linked lists in a perfectly safe way, but without references you can't really do that. Every time you index into an array with an integer variable you have the potential for failure; you're in the classic dynamic-typing position where a condition of your program's correctness is simultaneously too trivial for you to benefit from the compiler verifying it, and too difficult for you to be able to write down a proof the compiler can verify. In this case, though, the condition is that the index is within bounds, rather than that the object is of a type that supports the operation you're trying to apply to it.
To some extent, of course, you can avoid this with higher-order programming and/or list comprehensions. If your array type exposes an `each` function that invokes a block for each array element, for example, you don't need to bounds-check each invocation of `each`. But good luck writing heapsort or Gaussian elimination in terms of such methods!
I wonder if Alexandrescu's "range" abstraction, coupled with pattern-matching, might fill some of the gap?
I agree that Rust-style ownership tracking and borrowed references is a promising approach to the lifetime and garbage collection problem (though not the bounds-checking problem), but I don't really understand it yet; I've only written a few hundred lines of Rust. Rust itself is obviously at the other end of the complexity spectrum from Leconscrip, but affine typing itself is pretty simple. But I don't yet grok, for example, reference lifetime inference, or to what extent you could remove it and still have a usable language.
Damn, that kind of puts things in perspective. Is this a new idea?
I don't think hers uses actuarial tables, though.