As someone who has worked on a systems programming language for a long time, my strongest advice would be to avoid trying to make syntactic or semantic choices that are just different unless they're really motivated by making the systems aspect better, or to make the language more self-coherent. Having surprises and syntax to learn is a barrier to entry and probably won't impress anyone.
That is to say, do focus on systems problems. Key ones I identified are efficient data representation, avoiding needless memory churn/bloat, and talking directly to lower-level software/hardware, like the kernel.
Focus on systems programming and not on syntactic niceties or oddities.
Yes, I also found the description a little weird because of the emphasis on linear-time parsing. It is cool theoretically, and it could be understandable from a perspective of "make the compiler fast", but parsing is never the bottleneck in modern compilers. For a systems programming language this seems to be the wrong emphasis.
> . Unfortunately I also designed a language with top-level execution and nested functions - neither of which I could come up with a good compilation model for if I wanted to preserve the single-pass, no AST, no IR design of the compiler.
- This is the major PITA of C: Is an actual terrible target for transpilers that have aspirations and sophisticated features like Strings, some decent type system, or whatever. So, your option is to target something that is as close to semantics of your language (Basically, any other language that is not C), to the point where your target is MORE sophisticated than your own lang.
- I think Zig (for speed/apparent simplicity) and Rust could be good targets instead of C (I wish Rust were way faster to compile!). Assuming Zig, it will simplify other aspects like cross compiling
- I don't think is totally possible to avoid have a semi-interpreter for the transpiler, where you at most need a prelude with some hand-crafted functions that do the lifting. With this I mean things like `fn int(I:Int):Int'` so your code output is like `plus(int(1), int(2))`. Langs like APL/J use this for great effects and basically side-step the need for a vm/opcode. (see also: Compiling with closures)
> where your target is MORE sophisticated than your own lang.
I.. hadn't thought of this. I mean I wouldn't transpile to a slow lang like python but choosing Zig or C++ is tempting. Zig maybe not as it's unfinished. But C++ instead of C would make my life easier (for ex. implementing classes).
> prelude with some hand-crafted functions that do the lifting. With this I mean things like `fn int(I:Int):Int'` so your code output is like `plus(int(1), int(2))`
Curious what you mean here. Is it `1+2` -> ast Binop{'+', left, right} -> gen `plus(1,2)` ?? Sorry it's late here and I should sleep..
> Curious what you mean here. Is it `1+2` -> ast Binop{'+', left, right} -> gen `plus(1,2)` ?? Sorry it's late here and I should sleep..
Yes, `plus(1,2)`. The problem will become more apparent when you find things like `plus` need some overloading support/macros/generics/etc, so thing like `print` too.
So, eventually you need to think in macros, multiple versions of the same thing, or, if you craft things very carefully, only support things your target support so you can avoid it (but I wonder how much is feasible)
Nearly all of reddit is presently political spam, and bot spam - often political bot spam. Every subreddit has to some degree been infiltrated by political spam - even subs that have absolutely nothing to do with politics.
It's really unfortunate... reddit used to make me laugh - now it just makes me angry.
One of the issues with systems programming languages is that the definitions programmers use for "well-understood" terms vary wildly in actual practice.
For example, the term "side effects" has half a dozen different meanings in common use. A Haskell programmer wouldn't consider memory allocation to be a side effect. A realtime programmer might consider taking too long might be a side effect, hence tools like realtime sanitizer [0]. Cryptography developers often consider input-dependent timing variance a critical side effect [1]. Embedded developers often consider things like high stack usage to be a meaningful side-effect.
This isn't to say that a systems language needs to support all of these different definitions, just a suggestion that any systems language should be extremely clear about the use cases it's intending to enable and the definitions it uses.
>One of the issues with systems programming languages is that the definitions programmers use for "well-understood" terms vary wildly in actual practice.
I think he used side effect with a functional programming meaning. A pure function will just take immutable data and produce other immutable data without affecting state. A function which adds 1 to a number has no side effects, while a function that adds 1 to a number and prints to the console has side effects.
The term was introduced so long ago, it's basically prehistoric now. Pointers are needed only for system programming language, they can be absent in systems programming language.
I wish someone is inventing a systems programming language with a bit of safety but without a borrow checker. Is it even possible? Of course, having a garbage collector wouldn't qualify.
I like that everything starts with a keyword, it makes the language feel consistant and I assume the parser is simple to understand because of it.
I like that you distinguish a procedure from a function in regards to side-effects and that you support both mutable and immutable types.
I like that you dont have to put a semicolon after each line but instead just use newline.
I like that you don't need parenthesis for ifs and whiles, however I am not sure I like the while syntax. Maybe I need to try it a bit before I can make up my mind.
On the other hand I think the type system could be expanded to support more types of different sizes. Especially if you are going for a systems programming language you want to be able to control that.
I think you could have a nil type because it is handy but it would be good if the language somehow made sure that any variable that could potentially be nil has to be explicitly checked before use.
Since you need to use a `call` statement to invoke a function, is it possible to invoke a function inside of a function call? I.e. can you write `call f(g(x))` or `call f(call g(x))` or something like that?
Package managers are such an odd thing from a social perspective.
You'll see cases like NPM and to a lesser degree Cargo where projects have hefty dependency graphs because it is so easy to just pull in one more dependency, but on the other side you have C++ that has conan and vcpkg but the opinions on them are so mixed people rely on other methods like cmake fetch package instead.
I appreciate having tools that let me pull in what I need when I need it, but the dependency explosion is real and I dunno how to have one without the other.
If you require end users (and possibly libraries? IDK) to manually specify every transitive dependency of a dependency (but not hard-code/vendor it), this should act as a forcing function to reduce transitive dependency explosion in libraries (because it would degrade user experience). I'm not sure if users should have to update every dependency by hand (this discourages updates which can cause security bugs to persist, but automatic updates makes supply-chain attacks easier; AUR helpers generally diff PKGBUILDs before committing them, which partly protects against PKGBUILD but not source attacks, and even distros did not protect against the xz attack).
Another factor is that updating C++ compilers/stdlib tends to break older libraries; I'm not sure if this is any less the case in Rust (unclear? I mostly get trouble with C dependencies) or Python (old Numpy does not supply wheels for newer Python, and ruamel.yaml has some errors on newer Python: https://sourceforge.net/p/ruamel-yaml/tickets/476/).
To the best of my knowledge (I only dabble in Rust) there aren't often too many breaks unless code accidentally relied on soundness bugs which Rust makes 0 promise of retaining to keep code working.
I don't have to be worried that a 3rd party library without dependency begins to have 30 transitive dependencies which now can conflict with other diamond dependencies.
I need my dependency tree to be small to avoid every single factor of friction.
Language specific package manager is exactly what encourage the exponential explosion of packages leading to dependency hell (and lead to major security concerns).
>Language specific package manager is exactly what encourage the exponential explosion of packages leading to dependency hell (and lead to major security concerns).
For recreational programming purposes (and sometimes professional depending on the domain), they really are a distraction.
The existence of a package manager causes a social problem within the language community of excessive transitive dependencies. It makes it difficult to trust libraries and encourages bad habits.
Much like Rust has memory safety benefits as a result of some choices that make it difficult to work with in some context, lack of a package manager can have benefits that make it difficult to work with in certain contexts.
These are all just tradeoffs and I'm glad "no package manager" languages are still being created because I personally enjoy using them more.
I'd rather have my languages focus on being a language and use something non-language-specific like nix or bazel to situate the dependencies.
Sure, the language maintainers will need to provide some kind of api which can be called by the more general purpose tool, but why not have it be a first class citizen instead of some kind of foo2nix adapter maintained by a totally separate group of people?
There's no need to have a cozy CLI and a bespoke lockfile format and a dedicated package server when I'll be using other tools to handle those things in a non-language-specific way anyhow.
Big feature for me. In frontend dev, 3k dependencies in a hello world app is considered normal. In systems, a free-for-all dependency graph is a terrible plan, especially if it's an open ecosystem. NPM, Cargo, etc are good examples.
This is also why systems people will typically push back if you ask for non-official repos added to apt sources, etc.
As someone who has worked on a systems programming language for a long time, my strongest advice would be to avoid trying to make syntactic or semantic choices that are just different unless they're really motivated by making the systems aspect better, or to make the language more self-coherent. Having surprises and syntax to learn is a barrier to entry and probably won't impress anyone.
That is to say, do focus on systems problems. Key ones I identified are efficient data representation, avoiding needless memory churn/bloat, and talking directly to lower-level software/hardware, like the kernel.
Focus on systems programming and not on syntactic niceties or oddities.
Yes, I also found the description a little weird because of the emphasis on linear-time parsing. It is cool theoretically, and it could be understandable from a perspective of "make the compiler fast", but parsing is never the bottleneck in modern compilers. For a systems programming language this seems to be the wrong emphasis.
You have a lot of questions, so go to https://www.reddit.com/r/ProgrammingLanguages/
Some possible answers:
> . Unfortunately I also designed a language with top-level execution and nested functions - neither of which I could come up with a good compilation model for if I wanted to preserve the single-pass, no AST, no IR design of the compiler.
- This is the major PITA of C: Is an actual terrible target for transpilers that have aspirations and sophisticated features like Strings, some decent type system, or whatever. So, your option is to target something that is as close to semantics of your language (Basically, any other language that is not C), to the point where your target is MORE sophisticated than your own lang.
- I think Zig (for speed/apparent simplicity) and Rust could be good targets instead of C (I wish Rust were way faster to compile!). Assuming Zig, it will simplify other aspects like cross compiling
- I don't think is totally possible to avoid have a semi-interpreter for the transpiler, where you at most need a prelude with some hand-crafted functions that do the lifting. With this I mean things like `fn int(I:Int):Int'` so your code output is like `plus(int(1), int(2))`. Langs like APL/J use this for great effects and basically side-step the need for a vm/opcode. (see also: Compiling with closures)
> where your target is MORE sophisticated than your own lang.
I.. hadn't thought of this. I mean I wouldn't transpile to a slow lang like python but choosing Zig or C++ is tempting. Zig maybe not as it's unfinished. But C++ instead of C would make my life easier (for ex. implementing classes).
> prelude with some hand-crafted functions that do the lifting. With this I mean things like `fn int(I:Int):Int'` so your code output is like `plus(int(1), int(2))`
Curious what you mean here. Is it `1+2` -> ast Binop{'+', left, right} -> gen `plus(1,2)` ?? Sorry it's late here and I should sleep..
Nim might be useful, either as a compiler or transpiler.
https://hookrace.net/blog/introduction-to-metaprogramming-in...
https://livebook.manning.com/book/nim-in-action/chapter-9/
> Curious what you mean here. Is it `1+2` -> ast Binop{'+', left, right} -> gen `plus(1,2)` ?? Sorry it's late here and I should sleep..
Yes, `plus(1,2)`. The problem will become more apparent when you find things like `plus` need some overloading support/macros/generics/etc, so thing like `print` too.
So, eventually you need to think in macros, multiple versions of the same thing, or, if you craft things very carefully, only support things your target support so you can avoid it (but I wonder how much is feasible)
> Zig maybe not as it’s unfinished
But, but, but…
They share the same two start letters! They were clearly meant to cohabitate!
Zinc on Zig, what a Zing!
(Just a casual at-a-distance zig fan)
this subreddit suffers from insufferable admins that purged any useful activity and sense of community from this sub
Nearly all of reddit is presently political spam, and bot spam - often political bot spam. Every subreddit has to some degree been infiltrated by political spam - even subs that have absolutely nothing to do with politics.
It's really unfortunate... reddit used to make me laugh - now it just makes me angry.
One of the issues with systems programming languages is that the definitions programmers use for "well-understood" terms vary wildly in actual practice.
For example, the term "side effects" has half a dozen different meanings in common use. A Haskell programmer wouldn't consider memory allocation to be a side effect. A realtime programmer might consider taking too long might be a side effect, hence tools like realtime sanitizer [0]. Cryptography developers often consider input-dependent timing variance a critical side effect [1]. Embedded developers often consider things like high stack usage to be a meaningful side-effect.
This isn't to say that a systems language needs to support all of these different definitions, just a suggestion that any systems language should be extremely clear about the use cases it's intending to enable and the definitions it uses.
[0] https://clang.llvm.org/docs/RealtimeSanitizer.html
[1] https://www.bearssl.org/constanttime.html
>One of the issues with systems programming languages is that the definitions programmers use for "well-understood" terms vary wildly in actual practice.
I think he used side effect with a functional programming meaning. A pure function will just take immutable data and produce other immutable data without affecting state. A function which adds 1 to a number has no side effects, while a function that adds 1 to a number and prints to the console has side effects.
What makes this a "systems programming language", especially since it has "no pointers or references"?
I wonder the same, but aren't pointers just integers?
So if you store a memory address in the integer variable X, you just need a way to access that memory.
In assembly languages, usually, you have no pointers.
The term was introduced so long ago, it's basically prehistoric now. Pointers are needed only for system programming language, they can be absent in systems programming language.
Indeed, that makes it not a systems language by any definition I am familiar with.
> Reasonable C interop, and probably, initial compilation to C.
How do you achieve "reasonable C interop" without pointers, I wonder?
You cast integers to pointers and play with fire, of course!
Void*? Int. Char**? Also an Int. I take it back. This is true systems programming - the same type safety that raw assembly is known for.
I wish someone is inventing a systems programming language with a bit of safety but without a borrow checker. Is it even possible? Of course, having a garbage collector wouldn't qualify.
Already exists: https://verdagon.dev/blog/generational-references
I think thats Zig in the future - there's already allocators you can use that will detect some memory safety crimes
Sounds great!
I think you have something promising there.
I like that everything starts with a keyword, it makes the language feel consistant and I assume the parser is simple to understand because of it.
I like that you distinguish a procedure from a function in regards to side-effects and that you support both mutable and immutable types.
I like that you dont have to put a semicolon after each line but instead just use newline.
I like that you don't need parenthesis for ifs and whiles, however I am not sure I like the while syntax. Maybe I need to try it a bit before I can make up my mind.
On the other hand I think the type system could be expanded to support more types of different sizes. Especially if you are going for a systems programming language you want to be able to control that.
I think you could have a nil type because it is handy but it would be good if the language somehow made sure that any variable that could potentially be nil has to be explicitly checked before use.
I like where you're going with this.
If you haven't looked into Zig's 'comptime' system, you might find some relevant inspiration there.
Since you need to use a `call` statement to invoke a function, is it possible to invoke a function inside of a function call? I.e. can you write `call f(g(x))` or `call f(call g(x))` or something like that?
TIL. I learnt about the owl parser generator. Looks decent. Anyone's experience?
Is it called Zinc because zinc doesn’t easily corrode ;)?
This looks lovely, it's so readable!
> No package manager to distract you
What decade am I in? This is not optional any more. Hard pass.
Package managers are such an odd thing from a social perspective.
You'll see cases like NPM and to a lesser degree Cargo where projects have hefty dependency graphs because it is so easy to just pull in one more dependency, but on the other side you have C++ that has conan and vcpkg but the opinions on them are so mixed people rely on other methods like cmake fetch package instead.
I appreciate having tools that let me pull in what I need when I need it, but the dependency explosion is real and I dunno how to have one without the other.
If you require end users (and possibly libraries? IDK) to manually specify every transitive dependency of a dependency (but not hard-code/vendor it), this should act as a forcing function to reduce transitive dependency explosion in libraries (because it would degrade user experience). I'm not sure if users should have to update every dependency by hand (this discourages updates which can cause security bugs to persist, but automatic updates makes supply-chain attacks easier; AUR helpers generally diff PKGBUILDs before committing them, which partly protects against PKGBUILD but not source attacks, and even distros did not protect against the xz attack).
Another factor is that updating C++ compilers/stdlib tends to break older libraries; I'm not sure if this is any less the case in Rust (unclear? I mostly get trouble with C dependencies) or Python (old Numpy does not supply wheels for newer Python, and ruamel.yaml has some errors on newer Python: https://sourceforge.net/p/ruamel-yaml/tickets/476/).
To the best of my knowledge (I only dabble in Rust) there aren't often too many breaks unless code accidentally relied on soundness bugs which Rust makes 0 promise of retaining to keep code working.
Definitely a feature to me.
I don't have to be worried that a 3rd party library without dependency begins to have 30 transitive dependencies which now can conflict with other diamond dependencies.
I need my dependency tree to be small to avoid every single factor of friction.
Language specific package manager is exactly what encourage the exponential explosion of packages leading to dependency hell (and lead to major security concerns).
>Language specific package manager is exactly what encourage the exponential explosion of packages leading to dependency hell (and lead to major security concerns).
Sounds like you're biased.
https://archlinux.org/packages/extra/x86_64/gnome-shell/
gnome-shell > accountsservice > shadow > pam > systemd-libs > xz > bash > readline > ncurses > gcc-libs > glibc
and I didn't even try finding the longest chain...
For recreational programming purposes (and sometimes professional depending on the domain), they really are a distraction.
The existence of a package manager causes a social problem within the language community of excessive transitive dependencies. It makes it difficult to trust libraries and encourages bad habits.
Much like Rust has memory safety benefits as a result of some choices that make it difficult to work with in some context, lack of a package manager can have benefits that make it difficult to work with in certain contexts.
These are all just tradeoffs and I'm glad "no package manager" languages are still being created because I personally enjoy using them more.
I'd rather have my languages focus on being a language and use something non-language-specific like nix or bazel to situate the dependencies.
Sure, the language maintainers will need to provide some kind of api which can be called by the more general purpose tool, but why not have it be a first class citizen instead of some kind of foo2nix adapter maintained by a totally separate group of people?
There's no need to have a cozy CLI and a bespoke lockfile format and a dedicated package server when I'll be using other tools to handle those things in a non-language-specific way anyhow.
A tool like Nix also makes for an end result that's far more auditable than the latest and greatest language specific package manager of the day.
I am on the complete opposite side here. I detest language specific package managers for many reasons.
One of the non-goals is “to be useful to anyone,” after all.
I like this language, it shares my aesthetics.
It seems like the possible outcomes are:
(a) nobody uses the language, so a package manager doesn't matter OR
(b) people use the language, they will want to share packages, then a package manager will be bolted on (or many will, see python)
Seems like first-class package manager support (a la Rust) makes the most sense to me.
Big feature for me. In frontend dev, 3k dependencies in a hello world app is considered normal. In systems, a free-for-all dependency graph is a terrible plan, especially if it's an open ecosystem. NPM, Cargo, etc are good examples.
This is also why systems people will typically push back if you ask for non-official repos added to apt sources, etc.