[HN Gopher] Zen-C: Write like a high-level language, run like C
___________________________________________________________________
Zen-C: Write like a high-level language, run like C
Author : simonpure
Score : 146 points
Date : 2026-01-12 12:57 UTC (10 hours ago)
(HTM) web link (github.com)
(TXT) w3m dump (github.com)
| ethin wrote:
| Am I the only one who saw this syntax and immediately though
| "Man, this looks almost identical to Rust with a few slight
| variations"?
| CupricTea wrote:
| It seems to just be Rust for people who are allergic to using
| Rust.
|
| 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.
| nnevatie wrote:
| > what this adds
|
| I guess the point is what is subtracts, instead - answer
| being the borrow-checker.
| Ygg2 wrote:
| So it re-adds manual lifetime checking. Got it.
| askyng wrote:
| It might or might not be a toy project, I'm not sure, but
| one advantage of subtracting the borrow checking is that
| the compiler avoids a lot of complex machinery.
|
| 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.
| petcat wrote:
| > 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?
| the__alchemist wrote:
| Maybe take the parts of rust the author likes, but still
| encourages pointers in high level operations?
| suioir wrote:
| I thought the same and felt it looked really out of place to
| have I8 and F32 instead of i8 and f32 when so much else looks
| just like Rust. Especially when the rest of the types are all
| lower case.
| unwind wrote:
| Agreed, that really stood out as a ... questionable design
| decision, and felt extremely un-ergonomic which seems to go
| against the stated goals of the language.
| hyperhello wrote:
| Every language is apparently required to make one specific
| version of these totally arbitrary choices, like whether to
| call the keyword function, func, fun, fn, or def. Once they
| do, it's a foolish inconsistency with everything else. What
| if the language supported every syntax?
| turbotim wrote:
| My immediate thought was it looked a lot like Swift
| Gys wrote:
| Initial commit was 24h ago, 363 stars, 20 forks already. Man,
| this goes fast.
| worldsavior wrote:
| Could be bots.
| directmusic wrote:
| Definitely could be, but the dev has been posting updates on
| Twitter for a while now. It could be just some amount of hype
| they have built.
| alexpadula wrote:
| It's not, it's just how hackernews works. You'll see new
| projects hit 1k-10k stars in a matter of a day. You can have
| the best project, best article to you but if everyone else
| doesn't think so it'll always be at the bottom. Some luck
| involved too. Bots upvoting a post not organically I doubt is
| gonna live long on first page.
| worldsavior wrote:
| The stars are on GitHub, they can come from somewhere else,
| e.g. the author himself buying stars.
| alexpadula wrote:
| This is hella common. Companies have too much money to
| spend.
| Duendek86 wrote:
| Hi, I'm the developer's father. Trust me, he hasn't
| bought a single star in his life--not even in Super Mario
| :p
| saidnooneever wrote:
| man has been posting a lot before the initial commit about his
| library. following the guy on linkedin.
| giancarlostoro wrote:
| Syntax aside, how does this compare to Nim? Nim does similar, I
| think Crystal does as well? Not entirely sure about Crystal tbh.
| I guess Nim and Vala, since I believe both transpile to C, so you
| really get "like C" output from both.
| jdc0589 wrote:
| man I haven't heard anything about Vala in _ages_. is it still
| actively developed /used? how is it?
| spookie wrote:
| Yes, it is actively being developed.
|
| 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.
| lgunsch wrote:
| I was also going to mention this reminds me of Vala, which I
| haven't seen or heard from in 10+ years.
| giancarlostoro wrote:
| Surprisingly theres a shocking number of GUI programs for
| Linux made with Vala, and ElementaryOS is built using Vala,
| and all their custom software uses Vala. So it's not dead,
| just a little known interesting language. :)
| bloovis wrote:
| Crystal compiles directly to object code, using LLVM. It does
| provide the ability to interoperate with C code; as an example,
| I use this feature to call ncursesw functions from Crystal.
| GrowingSideways wrote:
| Why not compile to rust or assembly? C seems like an odd choice.
|
| In fact why not simply write rust to begin with?
| ndr wrote:
| At times people think C is better. See recent discussion about
| https://sqlite.org/whyc.html
| alexpadula wrote:
| C is best
| xnacly wrote:
| Assembly requires way more work than compiling to, say C. Clang
| and gcc do a lot of the heavy lifting regarding optimisation,
| spilling values to the stack, etc
| GrowingSideways wrote:
| Then you're stuck with the C stack, though, and no way to
| collect garbage.
| saidnooneever wrote:
| really? you cant track and count your pointers in C? why
| not?
| UncleEntity wrote:
| I have a couple interpreters I've been poking at and one
| uses 'musttail' while the other uses a trampoline to get
| around blowing up the C stack when dispatching the
| operators. As for the GC, the trampoline VM has a full-
| blown one (with bells-and-whistles like an arena for short
| lived intermediate results which get pushed/popped by the
| compiled instructions) while the other (a peg parser VM)
| just uses an arena as the 'evaluation' is short lived and
| the output is the only thing really needing tracking so
| uses reference counting to be (easily) compatible with the
| Python C-API. No worries about the C stack at all.
|
| 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.
| dfox wrote:
| If I understand the history correctly then it started as a set
| of C preprocessor macros.
| morcus wrote:
| An interesting bit to me is that it compiles to (apparently)
| readable C, I'm not sure how one would use that to their
| advantage
|
| 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?
| zbendefy wrote:
| i think so. The biggest hurdle with new languages is that you
| are cut off from a 3rdparty library ecosystem. Being compatible
| with C 3rd party libraries is a big win.
| actionfromafar wrote:
| Makes it easy to "try before you buy", too. If you decide
| it's not for you, you can "step out" and keep the generated C
| code and go from there.
| morcus wrote:
| Very good point that I never considered! Thanks.
| forgotpwd16 wrote:
| This isn't a very sane plan. The ~300 LOC example mini_grep
| (https://github.com/z-libs/Zen-C/blob/main/examples/tools/m
| in...) compiles to a ~3.3k LOC monstrosity
| (https://pastebin.com/raw/6FBSpt1z). It's easier to rewrite
| the whole thing than going from the generated code.
|
| At least for now, generated code shouldn't be considered
| something you're ever supposed to interact with.
| saidnooneever wrote:
| one benefit is that a lot of tooling e.g. for verification etc.
| is built around C.
|
| another is that it only has C runtime requirement, so no weird
| runtime stuff to impelement if youd say want to run on bare
| metal..you could output the C code and compile it to your
| target.
| v_iter wrote:
| So, the point of this language is to be able to write code with
| high productivity, but with the benefit of compiling it to a low
| level language? Overall it seems like the language repeats what
| ZIG does, including the C ABI support, manual memory management
| with additional ergonomics, comptime feature. The biggest
| difference that comes to mind quickly is that the creator of
| Zen-C states that it can allow for the productivity of a high
| level language.
| johnisgood wrote:
| Nim is a high-level language as well and compiles to C.
| the__alchemist wrote:
| Odin and Jai are others.
| Voycawojka wrote:
| Does Odin compile to C? I thought it only uses LLVM as a
| backend
| sestep wrote:
| Same question but for Jai.
| johnisgood wrote:
| No, Odin does not compile to C. It is a standalone
| programming language that compiles directly to machine
| code. It primarily uses LLVM as its backend for compiling
| to machine code, like you said.
| zem wrote:
| chicken scheme compiles to c as well. it's a pretty
| convenient compilation target, you get to use all the
| compilers and tool chains out there and you don't add a
| dependency on llvm
| dieggsy wrote:
| I love CHICKEN Scheme! Nice to see it mentioned. Though I
| think it's worth pointing out it compiles to something
| pretty far from handwritten C, to my understanding. I
| think this is true of both performance and semantics; for
| example you can return a pointer to a stack allocated
| struct from a foreign lambda (this is because chicken's
| generated C code here doesn't really "return", I think.
| Not an expert).
|
| 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.
| zem wrote:
| > 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...
| jrapdx3 wrote:
| Chicken indeed interoperates with C quite easily and
| productively. You're right that the generated C code is
| mostly incomprehensible to humans, but compiles without
| difficulty.
|
| 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.
| echelon wrote:
| There are going to be lots of languages competing with Rust and
| Zig. It's a popular, underserved market. They'll all have their
| unique angle.
| pjmlp wrote:
| I has been served for several decades, however since the
| late-90's many decided reducing to only C and C++ was the way
| going forward, now the world is rediscovering it doesn't have
| to be like that.
| forgotpwd16 wrote:
| They're are certainly going to be lots of languages because
| now with LLMs it's easier (trivial?) to make one + library
| (case in point: just within last month there're have been
| posted here ~20 new langs with codebases 20k~100k LOC) but
| don't really see them competing. Rust and Zig brought actual
| improvements and are basically replacing usecases that C++/C
| had limiting the space available to others.
| messe wrote:
| It has stringly typed macros. It's not comparable to Zig's
| comptime, even if it calls it comptime: 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.
| kuon wrote:
| I am working on mine as well. I think it is very sane to have
| some activity in this field. I hope we will have high level
| easy to write code that is fully optimized with very little
| effort.
| thomasmg wrote:
| I wonder, how can a programming language have the productivity
| of a high-level language ("write like a high-level language"),
| if it has manual memory management? This just doesn't add up in
| my view.
|
| 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
| alexpadula wrote:
| 18 commits! I hope you keep up with the project, it's really
| cool, great work.
| kreco wrote:
| That's a very nice project.
|
| 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.
| jdmichal wrote:
| C uses `|` for bitwise OR and `||` for logical OR. I'm assuming
| this inherited the same operator paradigm since it compiles to
| C.
| alfonsodev wrote:
| Is this the Typescript of C ?
| ramses0 wrote:
| The whole language examples seem pretty rational, and I'm
| especially pleased / shocked by the `loop / repeat 5` examples. I
| love the idea of having syntax support for "maximum number of
| iterations", eg: 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.
| zeknife wrote:
| Ruby has a similarly intuitive `3.times do ... end` syntax
| ioasuncvinvaer wrote:
| go also has for range 5 { ... }
| blacksqr wrote:
| What's the performance hit?
| Lucasoato wrote:
| > Mutability
|
| > By 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.
| andai wrote:
| Given an option that is configurable, why would the default
| setting be the one that increases probability of errors?
|
| 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 ;)
| miroljub wrote:
| > 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.
| Y_Y wrote:
| > I have considered such a flag for my future language, which
| I named #explode-randomly-at-runtime ;)
|
| A classic strategy!
|
| https://p-nand-q.com/programming/languages/java2k/
| psychoslave wrote:
| But why put it as a global metaswitcher instead of having
| different type infered from initial assignation qualifier?
|
| 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 float
| Dylan16807 wrote:
| > 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')
| netbioserror wrote:
| Yeah, immutability should probably use a `let` keyword and
| compiler analysis should enforce value semantics on those
| declarations.
| phcreery wrote:
| Agreed, using `var` keyword for something that is non-var-
| ying (aka immutable) is not very intuitive.
| gmueckl wrote:
| Other languages also have non-local qays of influencing
| compiler behavior, for example attributes in rust (standard) or
| compiler pragmas in C (non-standard).
|
| 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.
| maxbond wrote:
| It's not ideal but it seems like something an LSP could tell
| you on a hover event. I didn't see an LSP (I didn't look that
| hard either) but presumably that's within the scope of their
| mission statement to deliver modern language ergonomics. (But I
| agree with sibling comments that this should be a keyword.
| Another decent alternative would be that it's only global in
| scope.)
| forgotpwd16 wrote:
| Basically C2/C3 but Rust influenced. Missed chance to call it C4.
| seabrookmx wrote:
| It's odd that the async/await syntax _exclusively_ uses threads
| under the hood. I guess it makes for a straightforward
| implementation, but in every language I've seen the point of
| async/await is to use an event loop/cooperative multitasking.
| sestep wrote:
| Noob question: if it just compiles to threads, is there any
| need for special syntax in the first place? My understanding
| was that no language support should be required for blocking on
| a thread.
| gmueckl wrote:
| Async/await should do a little more under the hood than what
| the typical OS threading APIs provide, for example forwarding
| function parameters and return values automatically instead
| of making the user write their own boilerplate structs for
| that.
| Joker_vD wrote:
| From what I can see in the codegen, defer is not implemented
| "properly": the deferred statements are only executed when the
| block exits normally; leaving the block via "return", "break",
| "continue" (including their labelled variants! those interact
| subtly with outer defers), or "goto" skips them entirely. Which,
| arguably, should not happen: 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.
| ricardobeat wrote:
| Did you manage to compile this example?
| Joker_vD wrote:
| Yes, actually: $ 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); } }
| stevefolta wrote:
| Nice! Compiles in 2s on my unexceptional hardware. But it lacks
| my other main desiderata in a new language: string interpolation
| and kebab-case.
| taolson wrote:
| The author includes some easter-eggs (printing random facts about
| Zen and various C constructs) which trigger randomly -- check out
| the file src/zen/zen_facts.c in the repository...
| Dwedit wrote:
| I wonder how this compares to the Beef programming language.
|
| https://www.beeflang.org/
|
| The Beef programming language was used to write Penny's Big
| Breakaway.
| Zambyte wrote:
| The tagline also applies to C :-)
| aappleby wrote:
| Example at the top of the readme!
___________________________________________________________________
(page generated 2026-01-12 23:00 UTC)