Top
Best
New

Posted by todsacerdoti 10/29/2025

Zig's New Async I/O(andrewkelley.me)
https://www.youtube.com/watch?v=mdOxIc0HM04
329 points | 172 commentspage 2
synergy20 10/29/2025|
have not played with zig for a while, remain in the C world.

with the cleanup attribute(a cheap "defer" for C), and the sanitizers, static analysis tools, memory tagging extension(MTE) for memory safety at hardware level, etc, and a zig 1.0 still probably years away, what's the strong selling point that I need spend time with zig these days? Asking because I'm unsure if I should re-try it.

flohofwoe 10/29/2025||
My 2ct: A much more useful stdlib (that's also currently the most unstable part though, and I don't agree with all the design decisions in the stdlib - but mostly still better than a nearly useless stdlib like C provides - and don't even get me started about the C++ stdlib heh), integrated build system and package manager (even great for pure C/C++ projects), and comptime with all the things it enables (like straightforward reflection and generics).

It also fixes a shitton of tiny design warts that we've become accustomed to in C (also very slowly happening in the C standard, but that will take decades while Zig has those fixes now).

Also, probably the best integration with C code you can get outside of C++. E.g. hybrid C/Zig projects is a regular use case and has close to no friction.

C won't go away for me, but tinkering with Zig is just more fun :)

lukaslalinsky 10/30/2025|||
I really like Zig as a language, but I think the standard library is a huge weakness. I find the design extremely inconsistent. It feels like a collection of small packages, not a coherent unit. And I personally think this std.Io interface is on a similar path. The idea of abstracting our all I/O calls is great, but the actual interface is sketchy, in my opinion.
throwawaymaths 10/30/2025||
https://github.com/ziglang/zig/issues/1629
cassepipe 10/30/2025|||
Could you expand on what are the design decisions you disapprove of ? You got me curious
flohofwoe 10/30/2025||
Many parts of the stdlib don't separate between public interface and internal implementation details. It's possible to accidentially mess with data items which are clearly meant to be implementation-private.

I think this is because many parts of the stdlib are too object-oriented, for instance the containers are more or less C++ style objects, but this style of programming really needs RAII and restricted visibility rules like public/private (now Zig shouldn't get those, but IMHO the stdlib shouldn't pretend that those language features exist).

As a sister comment says, Zig is a great programming language, but the stdlib needs some sort of basic and consistent design philosophy whitch matches the language capabilities.

Tbf though, C gets around this problem by simply not providing a useful stdlib and delegating all the tricky design questions to library authors ;)

smj-edison 10/30/2025||
Honestly getting to mess with internal implementation details is my favorite part of using Zig's standard library. I'm working on a multithreaded interpreter right now, where each object has a unique index, so I need effectively a 4GB array to store them all. It's generally considered rude to allocate that all at once, so I'm using 4GB of virtual memory. I can literally just swap out std.MultiArrayList's backing array with vmem, set its capacity, and use all of its features, except now with virtual memory[1].

[1] https://github.com/smj-edison/zicl/blob/bacb08153305d5ba97fc...

flohofwoe 10/30/2025||
I would at least like to see some sort of naming convention for implementation-private properties, maybe like:

    const Bla = struct {
        // public access intended
        bla: i32,
        blub: i32,
        // here be dragons
        _private: struct {
            x: i32,
            y: i32,
        },
    };
...that way you can still access and mess up those 'private' items, but at least it's clear now which of the struct items are part of the 'public API contract' and which are considered internal implementation details which may change on a whim.
KallDrexx 10/30/2025|||
This is my main frustration with the push back against visibility modifiers. It's treated as an all or nothing approach, that any support for visibility modifiers locks anyone out from touching those fields.

It could just be a compiler error/warning that has to be explicitly opted into to touch those fields. This allows you to say "I know this is normally a footgun to modify these fields, and I might be violating an invariant condition, but I am know what I'm doing".

bsder 10/30/2025|||
"Visibility" invariably bites you in the ass at some point. See: Rust, newtypes and the Orphan rule, for example.

As such, I'm happy to not have visibility modifiers at all.

I do absolutely agree that "std" needs a good design pass to make it consistent--groveling in ".len" fields instead of ".len()" functions is definitely a bad idea. However, the nice part about Zig is that replacement doesn't need extra compiler support. Anyone can do that pass and everyone can then import and use it.

> This allows you to say "I know this is normally a footgun to modify these fields, and I might be violating an invariant condition, but I am know what I'm doing".

Welcome to "Zig will not have warnings." That's why it's all or nothing.

It's the single thing that absolutely grinds my gears about Zig. However, it's also probably the single thing that can be relaxed at a later date and not completely change the language. Consequently, I'm willing to put up with it given the rest of the goodness I get.

flohofwoe 10/31/2025|||
Tbf, C++/C# style public/private/protected is definitely too restricted in many situations, and other more flexible approaches are pretty much still research territory.
int_19h 10/31/2025||
They could do something like OCaml modules and signatures but more permissive. Module author writes the public signature and that's what the type checker runs against, but if you want, you can "downcast" the module to the actual signature, revealing implementation details.
smj-edison 10/30/2025||||
Aah, that's a good point. I suppose you could argue that the doc notes indicate whether something it meant to be accessed directly, like when ArrayList mentions "This field is intended to be accessed directly.", but that's really hard to determine at a glance.
pixelpoet 10/30/2025|||
How do you cope with the lack of vector operators? I am basically only writing vector code all day and night, and the lack of infix operators for vectors is just as unacceptable as not having them for normal ints and floats would be.

Nobody is asking for Pizza * Weather && (Lizard + Sleep), that strawman argument to justify ints and floats as the only algebraic types is infuriating :(

I'd love to read a blog post about your Zig setup and workflow BTW.

flohofwoe 10/31/2025|||
I'm coming from C so I got used to writing code like this ;)

    fn computeVsParams(rx: f32, ry: f32) shd.VsParams {
        const rxm = mat4.rotate(rx, .{ .x = 1.0, .y = 0.0, .z = 0.0 });
        const rym = mat4.rotate(ry, .{ .x = 0.0, .y = 1.0, .z = 0.0 });
        const model = mat4.mul(rxm, rym);
        const aspect = sapp.widthf() / sapp.heightf();
        const proj = mat4.persp(60.0, aspect, 0.01, 10.0);
        return shd.VsParams{ .mvp = mat4.mul(mat4.mul(proj, state.view), model) };
    }
Zig also has a `@Vector` type for SIMD-style vector math though, but I haven't been using that because sometimes I want to add methods to the vector types.

What I would like to see though is a Ziggified version of the Clang extended vector and matrix extensions.

smj-edison 10/31/2025|||
If you want to write really cursed code, you could use comptime to implement a function that takes comptime strings and parses them into vector function calls ;)
pixelpoet 10/31/2025||
I've actually thought about that, and looked a little into canonical/idiomatic ways to implement a DSL in Zig, and didn't find anything small and natural.

I just find it intellectually offensive that this extremely short-sighted line is drawn after ints and floats, any other algebraic/number types aren't similarly dignified.

If people had to write add(2, mul(3, 4)) etc for ints and floats the language would be used by exactly nobody! But just because particular language designers aren't using complex numbers and vectors all day, they interpret the request as wanting stupid abstract Monkey + Banana * Time or whatever. I really wish more Language People appreciated that there's only really one way to do complex numbers, it's worth doing right once and giving proper operators, too. Sure, use dot(a, b) and cross(a, b) etc, that's fine.

The word "number" is literally half of "complex number", and it's not like there are 1024 ways to implement 2D, 3D, 4D vector and complex number addition, subtraction, multiplication, maybe even division. There are many languages one can look to for guidance here, e.g. OpenCL[0] and Odin[1].

[0] OpenCL Quick Reference Card, masterpiece IMO: https://www.khronos.org/files/opencl-1-2-quick-reference-car...

[1] Odin language, specifically the operators: https://odin-lang.org/docs/overview/#operators

pron 10/30/2025||
I don't think any language that's not well-established, let alone one that isn't stabilised yet, would have a strong selling point if what you're looking for right now is to write production code that you'll maintain for a decade or more to come (I mean, companies do use languages that aren't as established as C in production apps, including Zig, but that's certainly not for everyone).

But if you're open to learning languages for tinkering/education purposes, I would say that Zig has several significant "intrinsic" advantages compared to C.

* It's much more expressive (it's at least as expressive as C++), while still being a very simple language (you can learn it fully in a few days).

* Its cross-compilation tooling is something of a marvel.

* It offers not only spatial memory safety, but protection from other kinds of undefined behaviour, in the form of things like tagged unions.

HacklesRaised 10/31/2025||
To the extent that it will affect ONLY my opinion of the language I reserve judgement, but prima facie, I'm a little worried that this will prove to be a distraction.

If the language didn't have a std implementation of async/await, would it affect your decision to use Xig? Or are you comfortable with traditional multithreaded facilities to the extent that you require concurrency (and parallelism)?

nialv7 10/31/2025||
This just sounds like a threadpool implementation to me. Sure you can brand it async, but it's not what we generally mean when we say async.
flohofwoe 10/31/2025||
Thread-pool is just one implementation under that new async interface. There are other implementations planned which to JS-style async/await (e.g. stackless coroutines) or green threads (stackful coroutines).
negrel 10/31/2025||
Many async implementation are still based on thread pools. rust's tokio, node's libuv uses a thread pool for file I/O.

Before io_uring (which is disabled on most servers until it mature), there was no good way to do async I/O on file on Linux.

nialv7 10/31/2025||
Sure async might be implemented by threadpools, but the threadpool itself is not what we usually refer to when we talk about async as a language feature.

e.g. Rust async may be backed by a threadpool, but it could also run single threaded.

sanity 10/29/2025||
Andrew Kelley is one of my all-time favorite technical speakers, and zig is packed full of great ideas. He also seems to be a great model of an open source project leader.
throwawaymaths 10/30/2025|
I would say zig is not just packed full of great ideas, there's a whole graveyard of ideas that were thrown out (and a universe of ideas that were rejected out of hand). Zig is full of great ideas that compose well together.
bcardarella 10/30/2025||
One that was tossed in 0.15 was `usingnamespace` which was a bit rough to refactor away from.
ancarda 10/29/2025||
I really hope this is going to be entirely optional, but I know realistically it just won't be. If Rust is any example, a language that has optional async support, async will permeate into the whole ecosystem. That's to be expected with colored functions. The stdlib isn't too bad but last time I checked a lot of crates.io is filled with async functions for stuff that doesn't actually block.

Async clearly works for many people, I do fully understand people who can't get their heads around threads and prefer async. It's wonderful that there's a pattern people can use to be productive!

For whatever reason, async just doesn't work for me. I don't feel comfortable using it and at this point I've been trying on and off for probably 10+ years now. Maybe it's never going to happen. I'm much more comfortable with threads, mutex locks, channels, Erlang style concurrency, nurseries -- literally ANYTHING but async. All of those are very understandable to me and I've built production systems with all of those.

I hope when Zig reaches 1.0 I'll be able to use it. I started learning it earlier this month and it's been really enjoyable to use.

eddd-ddde 10/29/2025||
> I do fully understand people who can't get their heads around threads and prefer async.

Those are independent of each other. You can have async with and without threads. You can have threads with and without async.

rowanG077 10/29/2025||
Yeah, it always mystifies me when people talking about async vs threads when they are completely orthogonal concepts. It doesn't give me the feeling they understand what they are talking about.
int_19h 10/31/2025||
It's because JavaScript was the thing that popularized async/await for many developers, and they aren't aware that it originated in a language (C#) and runtime (.NET) that are fully multithreaded.
lukaslalinsky 10/30/2025|||
It's optional to the point that you can write single-threaded version without any io.async/io.concurrent, but you will need to pass the io parameter around, if you want to do I/O. You are mistaking what is called "async" here for what other languages call async/await. It's a very different concept. Async in this context means just "spawn this function in a background, but if you can't, just run it right now".
flohofwoe 10/29/2025|||
> I'm much more comfortable with threads

The example code shown in the first few minutes of the video is actually using regular OS threads for running the async code ;)

The whole thing is quite similar to the Zig allocator philosophy. Just like an application already picks a root allocator to pass down into libraries, it now also picks an IO implementation and passes it down. A library in turn doesn't care about how async is implemented by the IO system, it just calls into the IO implementation it got handed from the application.

throwawaymaths 10/30/2025||
or you can pick one manually. Or, you can pick more than one and use as needed in different parts of your application. (probably less of a thing for IO than allocator).
audunw 10/29/2025|||
You don’t have to hope. Avoiding function colours and being able to write libraries that are agnostic to whether the IO is async or not is one of the top priorities of this new IO implementation.

If you don’t want to use async/await just don’t call functions through io.async.

pkulak 10/30/2025|||
> can't get their heads around threads and prefer async

Wow. Do you expect anyone to continue reading after a comment like that?

01HNNWZ0MV43FF 10/29/2025|||
I understand threads but I like using async for certain things.

If I had a web service using threads, would I map each request to one thread in a thread pool? It seems like a waste of OS resources when the IO multiplexing can be done without OS threads.

> last time I checked a lot of crates.io is filled with async functions for stuff that doesn't actually block.

Like what? Even file I/O blocks for large files on slow devices, so something like async tarball handling has a use case.

It's best to write in the sans-IO style and then your threading or async can be a thin layer on top that drives a dumb state machine. But in practice I find that passable sans-IO code is harder to write than passable async. It makes a lot of sense for a deep indirect dependency like an HTTP library, but less sense for an app

hugs 10/29/2025|||
same. i don't like async. i don't like having to prepend "await" to every line of code. instead, lately (in js), i've been playing more with worker threads, message passing, and the "atomics" api. i get the benefits of concurrency without the extra async/await-everywhere baggage.
iknowstuff 10/29/2025||
lol it’s just a very different tradeoff. Especially in js those approaches are far more of a “baggage” than async/await
hugs 10/29/2025||
probably, but i'm petty like that. i just really don't like async/await. i'm looking forward to eventually being punished for that opinion!
speed_spread 10/29/2025|||
I agree, async is way more popular than it should be. At least (AFAIU) Zig doesn't color functions so there won't be a big ecosystem rift between blocking and async libraries.
tomck 10/29/2025|||
> I do fully understand people who can't get their heads around threads and prefer async

This is a bizarre remark

Async/await isn't "for when you can't get your head around threads", it's a completely orthogonal concept

Case in point: javascript has async/await, but everything is singlethreaded, there is no parallelism

Async/await is basically just coroutines/generators underneath.

Phrasing async as 'for people who can't get their heads around threads' makes it sound like you're just insecure that you never learned how async works yet, and instead of just sitting down + learning it you would rather compensate

Async is probably a more complex model than threads/fibers for expressing concurrency. It's fine to say that, it's fine to not have learned it if that works for you, but it's silly to put one above the other as if understanding threads makes async/await irrelevant

> The stdlib isn't too bad but last time I checked a lot of crates.io is filled with async functions for stuff that doesn't actually block

Can you provide an example? I haven't found that to be the case last time I used rust, but I don't use rust a great deal anymore

dang 10/30/2025|||
> This is a bizarre remark

> makes it sound like you're just insecure

> instead of just sitting down + learning it you would rather compensate

Can you please edit out swipes like these from your HN posts? This is in the site guidelines: https://news.ycombinator.com/newsguidelines.html.

Your comment would be fine without those bits.

ksec 10/29/2025||||
>Case in point: javascript has async/await, but everything is singlethreaded, there is no parallelism, Async/await is basically just coroutines/generators underneath.

May be I just wish Zig dont call it async and use a different name.

vjerancrnjak 10/29/2025|||
It's more about how the code ends up evolving.

Async-await in JS is sometimes used to swallow exceptions. It's very often used to do 1 thing at a time when N things could be done instead. It serializes the execution a lot when it could be concurrent.

    if (await is_something_true()) {
      // here is_something_true() can be false
    }
And above, the most common mistake.

Similar side-effects happen in other languages that have async-await sugar.

It smells as bad as the Zig file interface with intermediate buffers reading/writing to OS buffers until everything is a buffer 10 steps below.

It's fun for small programs but you really have to be very strict to not have it go wrong (performance, correctness).

tomck 10/29/2025||
I think you replied to the wrong person.

That being said, I don't understand your `is_something_true` example.

> It's very often used to do 1 thing at a time when N things could be done instead

That's true, but I don't think e.g. fibres fare any better here. I would say that expressing that type of parallel execution is much more convenient with async/await and Promise.all() or whatever alternative, compared to e.g. raw promises or fibres.

vjerancrnjak 10/31/2025||
I replied directly to you. There are valid arguments as to why async-await sucks, even after you're deeply familiar with how it works. Even though it's just generators/coroutines beneath, async-await pollutes the code completely if you're not strict about its usage.

`is_something_true` is very simple, if condition is true, and then inside the block, if you were to check again it can be false, something that can't happen in synchronous code, yet now, with async-await, it's very easy to get yourself into situations like these, even though the code seems to yell at you that you're in the true branch. the solution is adding a lock, but with such ease to write async-await, it's rarely caught

tomck 10/31/2025||
> I replied directly to you

My comment was responding only to the person who equated threads and async. My comment only said that async and threading are completely orthogonal, even though they are often conflated

> `is_something_true` is very simple, if condition is true, and then inside the block, if you were to check again it can be false, something that can't happen in synchronous code

It can happen in synchronous code, but even if it couldn't - why is async/await the problem here? what is your alternative to async/await to express concurrency?

Here are the ways it can happen:

1. it can happen with fibers, coroutines, threads, callbacks, promises, any other expression of concurrency (parallel or not!). I don't understand why async/await specifically is to blame here.

2. Even without concurrency, you can mutate state to make the value of is_something_true() change.

3. is_something_true might be a blocking call to some OS resource, file, etc - e.g. the classic `if (file_exists(f)) open(f)` bug.

I am neutral on async/await, but your example isn't a very good argument against it

Seemingly nobody ever has any good arguments against it

> async-await pollutes the code completely if you're not strict about its usage

This is a good thing, if a function is async then it does something that won't complete after the function call. I don't understand this argument about 'coloured functions' polluting code. if a function at the bottom of your callstack needs to do something and wait on it, then you need to wait on it for all functions above.

If the alternative is just 'spin up an OS thread' or 'spin up a fiber' so that the function at the bottom of the callstack can block - that's exactly the same as before, you're just lying to yourself about your code. Guess what - you can achieve the same thing by putting 'await' before every function call

Perhaps you have convinced me that async/await is great after all!

vjerancrnjak 10/31/2025||
Async and threading are applied to similar problems, that's how I understood OP.

`if (file_exists(f))` is misuse of the interface, a lesson in interface design, and a faulty pattern that's easy to repeat with async-await.

> I don't understand this argument about 'coloured functions' polluting code.

Let's say you have some state that needs to be available for the program. What happens in the end game is that you've completely unrolled the loads and computation because any previous interface sucks (loading the whole state at the beginning which serializes your program and makes it slower, or defining `async loadPartial` that causes massive async-await pollution at places where you want to read a part of the state, even if it is already in cache).

Think about it, you can't `await` on part of the state only once and then know it's available in other parts of code to avoid async pollution. When you solve this problem, you realize `await` was just in the way and is completely useless and code looks exactly like a callback or any other more primitive mechanism.

A different example is writing a handler of 1 HTTP request. People do it all the time with async-await, but what to do when you receive N HTTP requests? The way to make code perform well is impossible with the serial code that deals with 1 request, so async-await just allowed you to make something very simple and in an easy way, but then it falls apart completely when you go from 1 to N. Pipelining system won't really care for async-await at all, even though it pipelines IO in addition to compute.

I think "how to express concurrency" is a question I'm not even trying to answer, although I could point to approaches that completely eliminate pollution and force you to write code in that "unrolled" way from start, something like Rx or FRP where time is exactly the unit they're dealing with.

tomck 10/31/2025||
> `if (file_exists(f))` is misuse of the interface, a lesson in interface design, and a faulty pattern that's easy to repeat with async-await.

It's even easier to repeat without async-await, where you don't need to tag the function call with `await`!

> Think about it, you can't `await` on part of the state only once and then know it's available in other parts of code to avoid async pollution. When you solve this problem, you realize `await` was just in the way and is completely useless and code looks exactly like a callback or any other more primitive mechanism.

I don't understand why you can't do this by just bypassing the async/await mechanism when you're sure that the data is already loaded

```

data = null

async function getDataOrWait() { await data_is_non_null(); // however you do this return data }

function getData() { if (data == null) { throw new Error('data not available yet'); } return data; }

```

You aren't forced into using async/await everywhere all the time. this sounds like 'a misuse of the interface, a lesson in interface design', etc

> I think "how to express concurrency" is a question I'm not even trying to answer

You can't criticise async/await, which is explicitly a way to express concurrency, if you don't even care to answer the question - you're just complaining about a solution that solves a problem that you clearly don't have (if you don't need to express concurrency, then you don't need async/await, correct!)

> point to approaches that completely eliminate pollution and force you to write code in that "unrolled" way from start, something like Rx or FRP where time is exactly the unit they're dealing with.

So they don't 'eliminate pollution', they just pollute everything by default all the time (???)

vjerancrnjak 10/31/2025||
> You aren't forced into using async/await everywhere all the time. this sounds like 'a misuse of the interface, a lesson in interface design', etc

Exactly, async-await does not allow you to create correct interfaces. You cannot write code that partially loads and then has sync access, without silly error raises sprinkled all over the place when you're 100% sure there's no way the error will raise or when you want to write code that is 100% correct and will access the part of the state when it is available.

Your example is obvious code pollution. For "correctness" sake I need to handle your raise even though it should not ever happen, or at least the null, to satisfy a type checker.

> So they don't 'eliminate pollution', they just pollute everything by default all the time (???)

That's not the case at all. They just push you immediately in direction where you'll land when you stop using async-await to enforce correctness and performance. edit: you stop modelling control flow and start thinking of your data dependency/computation graph where it's very easy and correct to just change computation/loads to batch mode. `is_something_true` example is immediately reframed to be correct all of the time (as a `file_exists` is now a signal and will fire on true and false)

> You can't criticise async/await, which is explicitly a way to express concurrency, if you don't even care to answer the question - you're just complaining about a solution that solves a problem that you clearly don't have (if you don't need to express concurrency, then you don't need async/await, correct!)

I'm critical of async-await, I'm not comparing it to something else, I can do that but I don't think it is necessary. I've pointed to FRP as a complete solution to the problem, where you're forced to deal with the dataflow from the start, for correctness sake, and can immediately batch, for pipelining/performance sake.

IMO, just like file_exists is a gimmick, or read_bytes(buffer), or write_bytes(buffer) is a leaky abstraction where now your program is required to manage unnecessary wasteful buffers, async-await pushes you into completely useless coding rituals, and your throw is a great example. The way you achieved middleground is with code pollution, because either full async load at the beginning is not performing well, or async-await interleaved with computation pollutes everything to be async and makes timeline difficult to reason about.

This late in the game, any concurrency solution should avoid pointless rituals.

duped 10/29/2025||
It sounds like your trouble with async is mistaking concurrency for parallelism.
Maxatar 10/29/2025||
Weird claim since threads were originally introduced as a concurrency primitive, basically a way to make user facing programs more responsive while sharing the same address space and CPU.

The idea of generalizing threads for use in parallel computing/SMP didn't come until at least a decade after the introduction of threads for use as a concurrency tool.

metaltyphoon 10/30/2025||
> Weird claim since threads were originally introduced as a concurrency primitive

Wasn't this only true when CPUs were single core only? Only when multi core CPUs came, true parallelism could happen (outside of using multiple CPUs)

gethly 10/29/2025||
I almost fell out of my chair at 22:06 from laughter :D
yohbho 10/30/2025|
due to broken audio?
hinkley 10/30/2025||
Not broken, that's Charlie Brown Teacher.

I guess they didn't get a release from the question asker and so they edited it out?

The problem here though is that the presenter didn't repeat the question for the audience which is a rookie mistake.

AndyKelley 10/29/2025||
text version: https://andrewkelley.me/post/zig-new-async-io-text-version.h...
geon 10/31/2025||
> Now that it's only using one thread, it deadlocks, because the consumer is waiting to get something from the queue, and the producer is scheduled to run, but it has not run yet.

Is the reason that the queue has a zero buffer size (always "full"), so that the consumer must consume the `flavor_text` before the producer can return?

Queue source:

> /// When buffer is full, producers suspend and are resumed by consumers.

https://github.com/ziglang/zig/blob/master/lib/std/Io.zig#L1...

dang 10/30/2025||
Thanks - we'll switch the URL to that above and re-up the thread.
pragmatic 10/30/2025||
https://andrewkelley.me/post/zig-new-async-io-text-version.h...

Text version.

The desynced video makes the video a bit painful to watch.

dang 10/30/2025|
Yup, we'll use the text version and keep the video link in the toptext.
MRson 10/30/2025|
I don't understand, why is allocation not part of IO? This seems like effect oriented programming with a kinda strange grouping: allocation, and the rest (io).
andyferris 10/30/2025|
Most effect-tracking languages have a GC and allocations/deallocations are handled implicitly.

To perform what we normally call "pure" code requires an allocator but not the `io` object. Code which accepts neither is also allocation-free - something which is a bit of a challenge to enforce in many languages, but just falls out here (from the lack of closures or globally scoped allocating/effectful functions - though I'm not sure whether `io` or an allocator is now required to call arbitrary extern (i.e. C) functions or not, which you'd need for a 100% "sandbox").

More comments...