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.
maxov 19 hours ago [-]
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.
muth02446 7 hours ago [-]
While working on my systems PL, Cwerg, I adopted a "syntax last" approach:
For the longest time the syntax was just glorified s-exprs. This made it much easier to focus on the semantic choices and improved iteration times and willingness to experiment since the parser changes were always trivial.
I highly recommend this approach for new PLs.
titzer 4 hours ago [-]
Interesting.
For Virgil, I started with mostly Java/C syntax, but with "variable: type" instead of "type variable", because it was both easier to parse and was more like standard ML and what you encounter in programming language theory. That syntax was already catching on, so I felt like I was swimming with the stream. I initially made silly changes like array indexing being "array(index)" instead of "array[index]", which turned out to be annoying to just take random code and change all the "[" to "(" and "]" to ")". Also, I had keywords "method" and "field", but eventually decided things looked better as "def" and "var", because they were easier to eyeball and readily understandable to people who write JavaScript (and Scala, as it turns out).
Overall Virgil's syntax is a kind of an average of all the curly braced languages and where it differs at all, it's been to make things more composable and avoid cryptic line-noise-looking things. For example, to allocate an object of class C, one writes "C.new(args)", because that can be understood as "C.new" as a function applied to "(args)"--so one can easily write "C.new" and yes, indeed, that's a first class function. That works with delegates and so on. So I don't regret not exactly matching the "new C()" you'd find in Java or C++.
dontlaugh 6 hours ago [-]
The danger is you might decide to just keep the s-exprs.
> . 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)
alcover 20 hours ago [-]
> 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..
rerdavies 10 hours ago [-]
I've been down this path. Code generation is great; but the downside to this approach is that making the language debug-able is pretty much impossible. (An MPEG-SA structured audio compiler with a C++ back end).
unquietwiki 19 hours ago [-]
Nim might be useful, either as a compiler or transpiler.
> 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)
travisgriggs 19 hours ago [-]
> 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)
artemonster 22 hours ago [-]
this subreddit suffers from insufferable admins that purged any useful activity and sense of community from this sub
jbreckmckye 5 hours ago [-]
How do you mean?
2 hours ago [-]
Alupis 21 hours ago [-]
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.
DaiPlusPlus 3 hours ago [-]
> Nearly all of reddit is presently political spam
The good (or at least barely tolerable) sane politics/econ subs hide in plain sight. (Also, enn cee dee?)
AlotOfReading 20 hours ago [-]
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.
Even if you limit "side effects" to observable behavior in the abstract machine sense, it's not entirely clear what is meant by a function to be "pure".
GCC has two attributes for marking functions, "pure" and "const" (not the language const qualifier). C23 introduced the [[reproducible]] and [[unsequenced]] attributes, that are mostly modeled by the GCC extensions, but with some subtle but important differences in their description.
Turns out it's pretty hard to define these concepts if the language is not built around immutability and pure functions from the ground up.
dontlaugh 10 hours ago [-]
I've never seen anyone actually refer to time variance for either realtime or crypto as "side effects".
It's true that these are all somewhat related concepts, but I'm pretty sure the term "side effect" is consistently used in the functional sense.
SkiFire13 1 hours ago [-]
The "functional sense" is the one that's underspecified for system programming. For example it considers allocation a pure operation, but that's actually implemented by modifying a global variable so how is it pure? One might argue that it's not observable, but so is printing to the console, which is usually taken as an example of an impure operation.
DeathArrow 13 hours ago [-]
>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.
Someone 11 hours ago [-]
> A pure function will just take immutable data and produce other immutable data without affecting state
But state is open for interpretation. If I write (making up syntax, attempting to be language-agnostic)
fnc foo uses scalar i produces scalar
does
return make scalar(i + 1)
end fnc
One could argue that is not pure, and one would have to write
fnc foo takes heap h, uses scalar i produces heap, integer
does
(newHeap, result) := heap.makeScalar(i + 1)
return (newHeap, result)
end fnc
That expresses the notion that this function destroys a heap and returns a new heap that stores an additional scalar (an implementation likely would optimize that to modify the heap that got passed in, but, to some, that’s an implementation detail)
> while a function that adds 1 to a number and prints to the console has side effects.
Again, that’s open for interpretation. If the program cannot read what’s on the console, why would that be considered a side effect? That function also heats my apartment and contributes my electricity bill.
Basic thing is: different programmers care about different things. Embedded programmers may care about minute details such as the number of cycles a function takes.
jerf 6 hours ago [-]
I'd also emphasize the point here is that if a systems-level programming language is going to call itself pure, it just needs a really, really careful definition of what exactly it means by pure. Purity is intrinsically relative [1]. That doesn't make it a bad goal, or a bad thing, and there's definitely a significant difference between a language striving for any meaning of "purity" versus one that doesn't care at all, but whatever definition the language designer is using should be very carefully defined. Particularly for a systems language, if by "systems language" one means "the sort of language that allows poking at low level details", because having lots of "low level" access greatly expands the scope of "things my code may be able to witness the stack for".
To give a degenerate-but-simple example of that, a low-level systems language striving for "purity" but that also allowed arbitrary memory reading for whatever reason ("deep low-level custom stack trace functionality") would technically be able to witness the effects of the stack changing due to function calls. You can just define that away as an effect (and honestly would probably have to), but I'd suggest being clear about it.
A denegenerate-but-often-relevant example is that a "pure" function in a language that considers memory allocation "pure" can crash the entire OS process by running it out of memory. That's so impure that not only can the execution context (thread, async context, whatever) that is running the program out of memory witness it, so can every other execution context in the process, indeed, whether they want to or not they have to! We generally consider memory allocation "pure" for pragmatic reasons, because we really have no choice, the alternative is to create such a restrictive definition of "pure" as to be effectively useless, but that is almost the largest possible "effect" we're glossing over!
What makes this a "systems programming language", especially since it has "no pointers or references"?
fc417fc802 18 hours ago [-]
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?
apgwoz 18 hours ago [-]
You cast integers to pointers and play with fire, of course!
fc417fc802 16 hours ago [-]
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.
pjmlp 8 hours ago [-]
Ironically Assembly is safer than C and languages that descend from it, because although CPUs might have undefined behaviour when given undocumented opcodes, or operation modes, the CPU doesn't rewrite your code without telling you about it.
AnimalMuppet 6 hours ago [-]
That's "safer" only against a very specific and limited set of dangers. But it opens the door to other dangers.
pjmlp 8 hours ago [-]
Naturally with PEEK and POKE.
DeathArrow 13 hours ago [-]
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.
tialaramex 12 hours ago [-]
Interestingly although all of C's other types are in fact just the machine integers wearing funny hats (e.g. char is just either a signed or unsigned byte depending on platform, float is just the 32-bit unsigned integers as binary fractions) the pointers are not actually just integers.
They could be, but it's much worse from a performance perspective if you just have these raw machine addresses rather than the pointers in the C language so actual C compilers haven't done that for many years. ISO/IEC TS 6010 describes the best current attempt to come up with coherent semantics for these pointers, or here's a Rustier perspective https://www.ralfj.de/blog/2020/12/14/provenance.html [today Rust specifically says its pointers have provenance and what that means, like that TS for the C language]
> float is just the 32-bit unsigned integers as binary fractions
Note that float and double are a bit particular because they can use different registers! But yeah, when stored in memory they are the same 32/63 bit integers.
pjmlp 8 hours ago [-]
Like we did in BASIC, with PEEK and POKE, just have to keep track what those numbers are for.
GoblinSlayer 13 hours ago [-]
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.
porridgeraisin 17 hours ago [-]
int ptr
Joker_vD 7 hours ago [-]
> The language splits side-effects from non-side-effecting code. Or rather would once functions and subroutines are implemented. Subroutines can have side-effects, functions cannot. Expressions can only apply functions, not call subroutines. It's like Haskell but better.
You mean, "worse". There is a reason why e.g. Pascal only had this misfeature in its very first version and gave up on it in the Revised Report, at which point having both functions and procedures arguably became an unnecessary distinction without difference.
> And there's always the issue of what to do about side-effects on module load.
You execute them. Just be sure to only run them once, and maintain proper traversal order (that being post-order): e.g. if your main program has "import A; import B", and A has "import B, import C", and B has "import D", you first run D's init, then B's, then C's, then main's.
duped 7 hours ago [-]
There's no meaningful order to shared dependencies or cyclic dependencies. You can pick one, but making it undefined is a lot more useful as the language implementer.
As a language user, don't rely on order for your side effects. Actually, just don't have side effects on module load. You almost never need it. Lazily initialize your globals.
Joker_vD 7 hours ago [-]
> There's no meaningful order to shared dependencies or cyclic dependencies.
Textual order. At least it's visible.
> Lazily initialize your globals.
What does that even mean? Something like that:
size_t _fwrite__impl(const void* buffer, size_t size, size_t count, FILE* stream) {
if (!__crt0_initialized) {
_crt0();
}
if (!__crt1_initialized) {
_crt1();
}
if (!__xfloat_initialized) {
_xfloat();
}
if (__stdio_initialized) {
_init_stdio();
}
// Actual implementation that touches internal FILE-tables and maybe does float/double formatting.
}
? But why?
> Actually, just don't have side effects on module load. You almost never need it.
See above. There is a surprising amount of invisibly initialized global state in e.g. C runtime library.
duped 6 hours ago [-]
> Textual order. At least it's visible.
Is "textual order" breadth-first, depth-first, reverse breadth-first, or reverse depth-first? Whichever you pick there will be a case where some module can't initialize because of assumptions it makes about how other modules are initialized. And like I said, it totally breaks down for cyclical dependencies - which are so common in practice, you must consider it.
> ? But why?
To paraphrase what Rust does, "no life before main." The point is to force expressions to be evaluated as they're used instead of as they're declared. There's an additional benefit that global resources that are not used are not initialized, which in the cases above has global side effects.
The glibc runtime is not something to be held up as a model for something particularly well designed. You can get all the benefits of hidden global initialization via laziness without all the problems placed on the programmer to care about their import declaration order, or undefined cases like cyclical imports.
One place where this stuff really sucks is when using dynamic linking and shared libraries have constructor functions that modify global state. GCC had to rollback changes to --ffast-math a couple years ago because loading two libraries compiled with different flags could result in undefined behavior when the MXCSR register depended on the order of library initialization.
Joker_vD 4 hours ago [-]
Textual order is depth-first. When you encounter "import X", you switch to (recursively) import/load module X. I believe that's how Python works.
As for the cyclical dependencies I'd argue they should be disallowed. Either their initialization order doesn't actually matter — in which case it doesn't matter :) — or there is a way to break things into smaller pieces and reorder them to function properly — in which case it's what should be done — or there is no valid ordering at all, in which case it's a genuine bug which has been made possible only because cyclical dependencies were allowed.
> There's an additional benefit that global resources that are not used are not initialized
This, arguably, can be considered a downside. Consider the security implications (and introduced mitigations) of e.g. writeable GOT/PLT. But it's a design decision with both of the choices valid, just with different trade-offs.
> You can get all the benefits of hidden global initialization via laziness without all the problems placed on the programmer to care about their import declaration order
I'd be interested to read about that. To me, this sounds mostly the problem of not accurately specifying your actual dependencies.
jezze 23 hours ago [-]
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.
DeathArrow 13 hours ago [-]
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 think thats Zig in the future - there's already allocators you can use that will detect some memory safety crimes
SkiFire13 11 minutes ago [-]
C/C++ also have various sanitizers but they are generally not used in production. Is Zig's allocator usable in production or only while developing?
DeathArrow 12 hours ago [-]
Sounds great!
marssaxman 22 hours ago [-]
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.
Cerium 7 hours ago [-]
It's called zinc because you are avoiding rust?
vram22 3 hours ago [-]
I wonder if there are PLs named after metals, or even other elements of the periodic table, other than Zinc, Carbon and Mercury? :)
SkiFire13 10 minutes ago [-]
Not a metal/element but there's Ruby
wk_end 21 hours ago [-]
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?
keyle 17 hours ago [-]
TIL. I learnt about the owl parser generator. Looks decent. Anyone's experience?
boguscoder 17 hours ago [-]
Is it called Zinc because zinc doesn’t easily corrode ;)?
moomin 3 hours ago [-]
Is it just me or is this sounding quite like BCPL?
lpapez 22 hours ago [-]
This looks lovely, it's so readable!
38 23 hours ago [-]
> No package manager to distract you
What decade am I in? This is not optional any more. Hard pass.
kreco 22 hours ago [-]
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).
imtringued 12 hours ago [-]
>Language specific package manager is exactly what encourage the exponential explosion of packages leading to dependency hell (and lead to major security concerns).
and I didn't even try finding the longest chain...
runevault 22 hours ago [-]
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.
nyanpasu64 21 hours ago [-]
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/).
rcxdude 11 hours ago [-]
This is optimizing for the wrong metric, IMO. If I look at the dependency tree of a fairly hefty project in rust, mostly what I see is the same amount of code as an equivalent project in C/C++, just split into multiple packages instead of bundled up into one source tree. Which ironically means packages tend to be able to pull in the minimal amount of excess code through transitive dependencies. All that you'll do with this kind of incentive is push packages into effectively vendoring their dependencies again.
runevault 17 hours ago [-]
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.
zifpanachr23 21 hours ago [-]
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.
__MatrixMan__ 22 hours ago [-]
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.
fc417fc802 18 hours ago [-]
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.
jezze 23 hours ago [-]
I am on the complete opposite side here. I detest language specific package managers for many reasons.
TimorousBestie 23 hours ago [-]
One of the non-goals is “to be useful to anyone,” after all.
I like this language, it shares my aesthetics.
philomath_mn 22 hours ago [-]
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.
parliament32 22 hours ago [-]
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.
Rendered at 20:19:47 GMT+0000 (Coordinated Universal Time) with Vercel.
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.
For the longest time the syntax was just glorified s-exprs. This made it much easier to focus on the semantic choices and improved iteration times and willingness to experiment since the parser changes were always trivial.
I highly recommend this approach for new PLs.
For Virgil, I started with mostly Java/C syntax, but with "variable: type" instead of "type variable", because it was both easier to parse and was more like standard ML and what you encounter in programming language theory. That syntax was already catching on, so I felt like I was swimming with the stream. I initially made silly changes like array indexing being "array(index)" instead of "array[index]", which turned out to be annoying to just take random code and change all the "[" to "(" and "]" to ")". Also, I had keywords "method" and "field", but eventually decided things looked better as "def" and "var", because they were easier to eyeball and readily understandable to people who write JavaScript (and Scala, as it turns out).
Overall Virgil's syntax is a kind of an average of all the curly braced languages and where it differs at all, it's been to make things more composable and avoid cryptic line-noise-looking things. For example, to allocate an object of class C, one writes "C.new(args)", because that can be understood as "C.new" as a function applied to "(args)"--so one can easily write "C.new" and yes, indeed, that's a first class function. That works with delegates and so on. So I don't regret not exactly matching the "new C()" you'd find in Java or C++.
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)
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..
https://hookrace.net/blog/introduction-to-metaprogramming-in...
https://livebook.manning.com/book/nim-in-action/chapter-9/
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)
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)
It's really unfortunate... reddit used to make me laugh - now it just makes me angry.
The good (or at least barely tolerable) sane politics/econ subs hide in plain sight. (Also, enn cee dee?)
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
GCC has two attributes for marking functions, "pure" and "const" (not the language const qualifier). C23 introduced the [[reproducible]] and [[unsequenced]] attributes, that are mostly modeled by the GCC extensions, but with some subtle but important differences in their description.
Turns out it's pretty hard to define these concepts if the language is not built around immutability and pure functions from the ground up.
It's true that these are all somewhat related concepts, but I'm pretty sure the term "side effect" is consistently used in the functional sense.
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.
But state is open for interpretation. If I write (making up syntax, attempting to be language-agnostic)
One could argue that is not pure, and one would have to write That expresses the notion that this function destroys a heap and returns a new heap that stores an additional scalar (an implementation likely would optimize that to modify the heap that got passed in, but, to some, that’s an implementation detail)> while a function that adds 1 to a number and prints to the console has side effects.
Again, that’s open for interpretation. If the program cannot read what’s on the console, why would that be considered a side effect? That function also heats my apartment and contributes my electricity bill.
Basic thing is: different programmers care about different things. Embedded programmers may care about minute details such as the number of cycles a function takes.
To give a degenerate-but-simple example of that, a low-level systems language striving for "purity" but that also allowed arbitrary memory reading for whatever reason ("deep low-level custom stack trace functionality") would technically be able to witness the effects of the stack changing due to function calls. You can just define that away as an effect (and honestly would probably have to), but I'd suggest being clear about it.
A denegenerate-but-often-relevant example is that a "pure" function in a language that considers memory allocation "pure" can crash the entire OS process by running it out of memory. That's so impure that not only can the execution context (thread, async context, whatever) that is running the program out of memory witness it, so can every other execution context in the process, indeed, whether they want to or not they have to! We generally consider memory allocation "pure" for pragmatic reasons, because we really have no choice, the alternative is to create such a restrictive definition of "pure" as to be effectively useless, but that is almost the largest possible "effect" we're glossing over!
[1]: https://jerf.org/iri/post/2025/fp_lessons_purity/#purity-is-...
> Reasonable C interop, and probably, initial compilation to C.
How do you achieve "reasonable C interop" without pointers, I wonder?
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.
They could be, but it's much worse from a performance perspective if you just have these raw machine addresses rather than the pointers in the C language so actual C compilers haven't done that for many years. ISO/IEC TS 6010 describes the best current attempt to come up with coherent semantics for these pointers, or here's a Rustier perspective https://www.ralfj.de/blog/2020/12/14/provenance.html [today Rust specifically says its pointers have provenance and what that means, like that TS for the C language]
Now, if you read Ralf's post and want to argue about that I'm afraid there are already lots of HN discussions and your point has probably already been made so: https://news.ycombinator.com/item?id=25419740 or https://news.ycombinator.com/item?id=42878450
Note that float and double are a bit particular because they can use different registers! But yeah, when stored in memory they are the same 32/63 bit integers.
You mean, "worse". There is a reason why e.g. Pascal only had this misfeature in its very first version and gave up on it in the Revised Report, at which point having both functions and procedures arguably became an unnecessary distinction without difference.
> And there's always the issue of what to do about side-effects on module load.
You execute them. Just be sure to only run them once, and maintain proper traversal order (that being post-order): e.g. if your main program has "import A; import B", and A has "import B, import C", and B has "import D", you first run D's init, then B's, then C's, then main's.
As a language user, don't rely on order for your side effects. Actually, just don't have side effects on module load. You almost never need it. Lazily initialize your globals.
Textual order. At least it's visible.
> Lazily initialize your globals.
What does that even mean? Something like that:
? But why?> Actually, just don't have side effects on module load. You almost never need it.
See above. There is a surprising amount of invisibly initialized global state in e.g. C runtime library.
Is "textual order" breadth-first, depth-first, reverse breadth-first, or reverse depth-first? Whichever you pick there will be a case where some module can't initialize because of assumptions it makes about how other modules are initialized. And like I said, it totally breaks down for cyclical dependencies - which are so common in practice, you must consider it.
> ? But why?
To paraphrase what Rust does, "no life before main." The point is to force expressions to be evaluated as they're used instead of as they're declared. There's an additional benefit that global resources that are not used are not initialized, which in the cases above has global side effects.
The glibc runtime is not something to be held up as a model for something particularly well designed. You can get all the benefits of hidden global initialization via laziness without all the problems placed on the programmer to care about their import declaration order, or undefined cases like cyclical imports.
One place where this stuff really sucks is when using dynamic linking and shared libraries have constructor functions that modify global state. GCC had to rollback changes to --ffast-math a couple years ago because loading two libraries compiled with different flags could result in undefined behavior when the MXCSR register depended on the order of library initialization.
As for the cyclical dependencies I'd argue they should be disallowed. Either their initialization order doesn't actually matter — in which case it doesn't matter :) — or there is a way to break things into smaller pieces and reorder them to function properly — in which case it's what should be done — or there is no valid ordering at all, in which case it's a genuine bug which has been made possible only because cyclical dependencies were allowed.
> There's an additional benefit that global resources that are not used are not initialized
This, arguably, can be considered a downside. Consider the security implications (and introduced mitigations) of e.g. writeable GOT/PLT. But it's a design decision with both of the choices valid, just with different trade-offs.
> You can get all the benefits of hidden global initialization via laziness without all the problems placed on the programmer to care about their import declaration order
I'd be interested to read about that. To me, this sounds mostly the problem of not accurately specifying your actual dependencies.
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.
If you haven't looked into Zig's 'comptime' system, you might find some relevant inspiration there.
What decade am I in? This is not optional any more. Hard pass.
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).
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...
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.
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/).
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.
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.
I like this language, it shares my aesthetics.
(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.
This is also why systems people will typically push back if you ask for non-official repos added to apt sources, etc.