Zen-C: Write like a high-level language, run like C
I'm writing my own programming language that tries "Write like a high-level language, run like C.", but it does not have manual memory management. It has reference counting with lightweight borrowing for performance sensitive parts: https://github.com/thomasmueller/bau-lang
I get your statement and even agree with it in certain contexts. But in a discussion where high-level languages are presumed (in context) to not have memory management, looping constructs are defined over a semantics inferred range of some given types, overloading of functions (maybe even operators), algebraic datatypes, and other functional language mixins: C most certainly IS NOT a high level language.
This is pedantic to the point of being derailing and in some ways seemed geared to end the discussion occurring by sticking a bar in the conversations spokes.
C was coined originally as high level because the alternatives were things like assembler. a term rooted in comparisson more than anything.
In that sense Zen-C changed too many things at once for no good reason. If it was just C with defer, there would have been an opportunity to include defer in the next release of the C standard.
fn main() {
comptime {
var N = 20;
var fib: long[20];
fib[0] = (long)0;
fib[1] = (long)1;
for var i=2; i<N; i+=1 {
fib[i] = fib[i-1] + fib[i-2];
}
printf("// Generated Fibonacci Sequence\n");
printf("var fibs: int[%d] = [", N);
for var i=0; i<N; i+=1 {
printf("%ld", fib[i]);
if (i < N-1) printf(", ");
}
printf("];\n");
}
print "Compile-time generated Fibonacci sequence:\n";
for i in 0..20 {
print f"fib[{i}] = {fibs[i]}\n";
}
}
It just literally outputs characters, not even tokens like rust's macros, into the compiler's view of the current source file. It has no access to type information, as Zig's does, and can't really be used for any sort of reflection as far as I can tell.The Zig equivalent of the above comptime block just be:
const fibs = comptime blk: {
var f:[20]u64 = undefined;
f[0] = 0;
f[1] = 1;
for (2..f.len) |i| {
f[i] = f[i-1] + f[i-2];
}
break :blk f;
};
Notice that there's no code generation step, the value is passed seamlessly from compile time to runtime code.Of course you can always drop to manually written C yourself and it's still a fantastic language to interop with C. And CHICKEN 6 (still pre-release) improves upon that! E.g structs and Unions can be returned/passed directly by/to foreign functions, and the new CRUNCH extension/subset is supposed to compile to something quite a bit closer to handwritten C; there are even people experimenting with it on embedded devices.
The Chicken C API has functions/macros that return values and those that don't return. The former include the fabulous embedded API (crunch is an altogether different beast) which I've used in "mixed language" programming to good effect. In such cases Scheme is rather like the essential "glue" that enables the parts written in other languages to work as a whole.
Of course becoming proficient in Scheme programming takes time and effort. I believe it's true that some brains have an affinity for Lispy languages while others don't. Fortunately, there are many ways to write programs to accomplish a given task.
this is because chicken's generated C code here doesn't really "return", I think. Not an expert.
not an expert either, but you're right about that, it uses cps transformations so that functions never return. there's a nice write up here: https://wiki.call-cc.org/chicken-compilation-process#a-guide...
V's approach is to have various backends, in addition to native (to be focused on from 0.6); C, JavaScript, WASM, etc...
MutabilityBy default, variables are mutable. You can enable Immutable by Default mode using a directive.
//> immutable-by-default
var x = 10; > // x = 20; // Error: x is immutable
var mut y = 10; > y = 20; // OK
Wait, but this means that if I’m reading somebody’s code, I won’t know if variables are mutable or not unless I read the whole file looking for such directive. Imagine if someone even defined custom directives, that doesn’t make it readable.
const f = (x) => {
const y = x + 1;
return y + 1;
}
y is an immutable variable. In f(3), y is 4, and in f(7), y is 8.I've only glanced at this Zen-C thing but I presume it's the same story.
In Rust if you define with "let x = 1;" it's an immutable variable, and same with Kotlin "val x = 1;"
Neither "let" nor "val[ue]" implies constancy or vacillation in themselves without further context.
Euler used this terminology, it's not new fangled corruption or anything. I'm not sure it makes too much sense to argue they new languages should use a different terminology than this based on a colloquial/nontechnical interpretation of the word.
Also it’s fine that anyone name things as it comes to their mind — as long as the other side get what is meant at least, I guess.
On the other it doesn’t hurt much anyone to call an oxymoron thus, or exchange in vacuous manner about terminology or its evolution.
On the specific example you give, I’m not an expert, but it seems dubious to me. In x+1=2, terms like x are called unknowns. Prove me wrong, but I would rather bet that Euler used unknown (quantitas incognita) unless he was specifically discussing variable quantities (quantitas variabilis) to describe, well, quantities that change. Probably he used also French and German equivalents, but if Euler spoke any English that’s not reflected in his publications.
The use of "variable" to denote an "unknown" is a very old practice that predates computers and programming languages.
In a function f(x), x is a variable because each time f is invoked, a different value can be provided for x. But that variable can be immutable within the body of the function. That’s what’s usually being referred to by “immutable variable”.
This terminology is used across many different languages, and has nothing to do with Javascript specifically. For example, it’s common to describe pure functional languages by saying something like “all variables are immutable” (https://wiki.haskell.org/A_brief_introduction_to_Haskell).
Good luck propagating ideas, as sound as it might, to a general audience once something is established in some jargon.
[1] https://en.wiktionary.org/wiki/mneme [2] https://www.merriam-webster.com/medical/mnemon
Probably variable is initially coming out of an ellipsis for something like "(possibly) variable* value stored in some dedicated memory location".
No, the term came directly from mathematics, where it had been firmly established by 1700 by people like Fermat, Newton, and Leibniz.
The confusion was introduced when programming languages decided to allow a variable's value to vary not just when a function was called, but during the evaluation of a function. This then creates the need to distinguish between a variable whose value doesn't change during any single evaluation of a function, and one that does change.
As I mentioned, the terms apply to two different aspects of the variable lifecycle, and that's implicitly understood. Saying it's an "oxymoron" is a version of the etymological fallacy that's ignoring the defined meanings of terms.
1) A ‘variable’ is an identifier which is bound to a fixed value by a definition;
2) a ‘variable’ is a memory location, or a higher level approximation abstracting over memory locations, which is set to and may be changed to a value by an assignment;
Both of the above are acceptable uses of the word. I am of the mindset that the non-independent existence of these two meanings in both languages and in discourse are a large and fundamental problem.
I take the position that, inspired by mathematics, a variable should mean #1. Thereby making variables immutably bound to a fixed value. Meaning #2 should have some other name and require explicit use thereof.
From the PLT and Maths background, a mutable variable is somewhat oxymoronic. So, I agree let’s not copy JavaScript, but let’s also not be dismissive of the usage of terminology that has long standing meanings (even when the varied meanings of a single term are quite opposite).
For some niches the answer is "because the convenience is worth it" (e.g. game jams). But I personally think the error prone option should be opt in for such cases.
Or to be blunt: correctness should not be opt-in. It should be opt-out.
I have considered such a flag for my future language, which I named #explode-randomly-at-runtime ;)
Given an option that is configurable, why would the default setting be the one that increases probability of errors?
They're objecting to the "given", though. They didn't comment either way on what the default should be.
Why should it be configurable? Who benefits from that? If it's to make it so people don't have to type "var mut" then replace that with something shorter!
(Also neither one is more 'correct')
I think "const x = something();" would be logical but they've used const already for compile-time constants. There's probably a sensible way of overloading that use though, depending if the expression would be constant at compile-time or not, but I've not considered it enough to think about edge cases (as it basically reduces to the halting problem unless any functions called are also explicitly marked up as compile time or not).
And "variables" in math are almost always immutable within a single invocation. It's not a particularly bad word to use. But there's plenty of options. const/var. let/var. let/mut. var/mut I guess. let/set from a sibling comment.
Or to be blunt: correctness should not be opt-in. It should be opt-out.
One can perfectly fine write correct programs using mutable variables. It's not a security feature, it's a design decision.
That being said, I agree with you that the author should decide if Zen-C should be either mutable or immutable by default, with special syntax for the other case. As it is now, it's confusing when reading code.
Example:
let integer answer be 42 — this is a constant
set integer temperature be 37.2 — this is a mutable
Or with the more esoglyphomaniac fashion cst ↦ 123 // a constant is just a trivial map
st ← 29.5 // initial assignment inferring floatI have considered such a flag for my future language, which I named #explode-randomly-at-runtime ;)
A classic strategy!
When reading working code, it doesn't matter whether the language mode allows variable reassignment. It only matters when you want to change it. And even then, the compiler will yell at you when you do the wrong thing. Testing it out is probably much faster than searching the codebase for a directive. It doesn't seem like a big deal to me.
It looks like a fun project, but I'm not sure what this adds to the point where people would actually use it over C or just going to Rust.
what this adds
I guess the point is what is subtracts, instead - answer being the borrow-checker.
Borrow checking in Rust isn't sound AFAIK, even after all these years, so some of the problems with designing and implementing lifetimes, region checking, and borrow checking algorithms, aren't trivial.
Borrow checking in Rust isn't sound AFAIK, even after all these years
Huh? If borrow checking in Rust is unsound, that's akin to saying Rust is utterly broken. Sounds like you've been fed FUD.
If Rust was that unsound, Rust haters would flood Twitter with Rust L takes.
The premise is too ridiculous to engage seriously. Apparently Google/AWS/MSFT/C++ engineers all missed a huge gaping hole in Rust borrow checker, something that random commenter could pick up.
answer being the borrow-checker
There is an entire world in Rust where you never have to touch the borrow-checker or lifetimes at all. You can just clone or move everything, or put everything in an Arc (which is what most other languages are doing anyway). It's very easy to not fight the compiler if you don't want to.
Maybe the real fix for Rust (for people that don't want to care), is just a compiler mode where everything is Arc-by-default?
I am not too familiar with C - is the idea that it's easier to incrementally have some parts of your codebase in this language, with other parts being in regular C?
At least for now, generated code shouldn't be considered something you're ever supposed to interact with.
Nim is a full, independent modern language that uses C as one of its backends. It has its own runtime, optional GC, Unicode strings, bounds checking, and a huge stdlib. You write high-level Nim code and it spits out optimized C you usually don't touch.
Here’s a little comparison I put together from what I can find in the readme and code:
Comparison ZenC Nim
written in C Self-Hosted
targets C C, C++, ObjC, JS, LLVM (via nlvm), Native (in-progress)
platforms POSIX Linux, Windows, MacOS, POSIX, baremetal
mm strategy manual/RAII ARC, ORC(ARC with cycle collector), multiple gc, manual
generated code human-readable optimized
mangling no yes
stdlib bare extensive/batteries-included
compile-time code yes yes
macros comptime? AST manipulation
arrays C arrays type and size is retained at all times
strings C strings have capacity and length, support Unicode
bounds-checking no yes (optional)Quite easy to make apps with it and GNOME Builder makes it really easy to package it for distribution (creates a proper flatpak environment, no need to make all the boilerplate). It's quite nice to work with, and make stuff happen. Gtk docs and awful deprecation culture (deprecate functions without any real alternative) are still a PITA though.
In fact why not simply write rust to begin with?
I mean, I could have used the C stack as the VM's stack but then you have to worry about blowing up the stack, not having access (without a bunch of hackery, looking at you scheme people) to the values on the stack for GC and whatnot and, I imagine, all the other things you have issues with but it's not needed at all, just make your own (or, you know, tail call) and pretend the C one doesn't exist.
And I've started on another VM which does the traditional stack thing but it's constrained (by the spec) to have a maximum stack depth so isn't too much trouble.
repeat 3 {
try { curl(...) && break }
except { continue }
}
...obviously not trying to start any holy wars around exceptions (which don't seem supported) or exponential backoff (or whatever), but I guess I'm kindof shocked that I haven't seen any other languages support what seems like an obvious syntax feature.I guess you could easily emulate it with `for x in range(3): ...break`, but `repeat 3: ...break` feels a bit more like that `print("-"*80)` feature but for loops.
var f = fopen("file.txt", "r");
defer fclose(f);
if fread(&ch, 1, 1, f) <= 0 { return -1; }
return 0;
would not close file if it was empty. In fact, I am not sure how it works even for normal "return 0": it looks like the deferred statements are emitted after the "return", textually, so they only properly work in void-returning function and internal blocks. $ cat kekw.zc
include <stdio.h>
fn main() {
var f = fopen("file.txt", "r");
defer fclose(f);
var ch: byte;
if fread(&ch, 1, 1, f) <= 0 { return -1; }
return 0;
}
$ ./zc --emit-c kekw.zc
[zc] Compiling kekw.zc...
$ tail -n 12 out.c
int main()
{
{
__auto_type f = fopen("file.txt", "r");
uint8_t ch;
if ((fread((&ch), 1, 1, f) <= 0)) {
return (-1);
}
return 0;
fclose(f);
}
}first your write 'tutorial C'. then after enough segfaults and double frees you start every project with a custom allocator because you've become obsessed with not having that again..., then you implement a library with a custom more generic one as you learn how to implement them, and add primitives you commonly build that lean on that allocator, it will have your refcouters, maybe ctors, dtors etc etc.. (this atleast is my learning path i guess? still have a loooong way to go as always!)
i dont see myself going for a language like this, but i think its inspirational to see where your code can evolve to with enough experience and care
So, having an async function run on a separate thread from those functions that are synchronous seems a viable way to achieve the underlying goal of continuous processing in the face of computations that involve waiting for some resource to become available.
I will agree that inspired by C#’s originating and then JavaScripts popularization of the syntax, it is not a stretch to assume async/await is implemented with an event loop (since both languages use such for implementation).
List of remarks:
var ints: int[5] = {1, 2, 3, 4, 5};var zeros: [int; 5]; // Zero-initialized
The zero initialized array is not intuitive IMO.
// Bitfields
If it's deterministically packed.
Tagged unions
Same, is the memory layout deterministic (and optimized)?
2 | 3 => print("Two or Three")
Any reason not to use "2 || 3"?
Traits
What if I want to remove or override the "trait Drawing for Circle" because the original implementation doesn't fit my constraints? As long as traits are not required to be in a totally different module than the struct I will likely never welcome them in a programming language.