Keyboard input

for Zig 0.15.2 Buy

Keyboard input

The player arrives in the year 2040 in this game. Everything is managed by massive Artificial Intelligences that control absolutely all services.

No one programs anymore-not even types. Not even talks to machines.

The last invention that revolutionized the world in 2030 was the near-telepathic brain link with the Central System, the “mindblowing” technology.

Humans are connected to the system, and robots serving the big corporations:  assembled, programmed, reprogrammed, updated, and even destroyed almost daily by the large AI models.

Knowledge and Truth are managed by the AI.

No one needs to do anything anymore.

No one knows how to do anything anymore.

But it seems that in the last update, something went wrong… despite all the safety measures, before any AI could even notice.

The system has bricked itself. Everything is affected, from the most basic modules to the core.

Technological homogenization is humanity’s greatest vulnerability.

The cities are frozen.

Humans are trapped in their links,

And robots are paralyzed in a bugged system.

        You think: Who could possibly fix this now? From the bottom? How can machines be made to understand, for the first time in years, something that isn’t abstract thought? The player finds an old, forgotten terminal-AI Interface crc32mode. The problem is, someone-or something-erased the command shell. But the terminal still has the system core: a self-sufficient microkernel. Maybe...

To read keyboard input, we have to solve two unknowns:

  1. We don’t know the content until the program runs.
  2. We don’t know the size of the data in advance.

This means we’re dealing with runtime data. As usual in Zig, we need to manage memory ourselves.

What we do know is that the input will be a sequence of ASCII characters, which in Zig are values of type u8.

In previous chapters, we’ve already seen strings defined statically, like this:

 print("Hello world!\n", .{});

or like this:

 const s_name_pow: []const u8 = "UNKNOWN";

We already know these are literal values - written directly - and therefore known at compile time. Now, the situation is different: the content is unknown.

Single-line input

The structure that holds a string is actually an array of bytes. The input size, as we said, is unknown, but we know that arrays have a fixed size. The first solution that comes to mind might be this: create a large enough array so that the input doesn’t overflow its size:

input.zig

const std = @import("std");

const print = std.debug.print;

// standard input is defined in Zig as a "file descriptor"

const stdin = std.fs.File.stdin();

// a "large enough" array to hold keyboard input

var a_input_buffer: [1024]u8 = undefined;

// the prompt marks the start of each terminal line

const s_prompt = "?> ";

pub fn main() !void {

    // print the prompt

    print("{s}", .{s_prompt});

    // when reading from stdin we use the try keyword because

    // it might fail and return an Error, or the number of bytes read

    const n_len = try stdin.read(&a_input_buffer);

    // create a slice from the input buffer

    const z_x = a_input_buffer[0 .. n_len - 1];

    // print what the user wrote

    print("{s}{s}\n", .{ s_prompt, z_x });

}

~$ zig run input.zig

?> Hello

?> Hello

We’ve managed to get the terminal to read our input and print it back to confirm it was received. It’s a good start!

Looped input

We can make the input more useful by turning it into a simple loop-based shell:

To do that, we use a while loop with a condition that's always true. The program keeps running until we stop it manually with Ctrl+C.

To keep things clean, we move the input logic into a separate readln function. That way, the code is easier to read, to debug, and easier to change later.

input_while.zig

const std = @import("std");

const print = std.debug.print;

const stdin = std.fs.File.stdin();

// read a line of up to 1024 characters and return the content

fn readln(o_alloc: std.mem.Allocator) ![]const u8 {

    var a_input_buffer: [1024]u8 = undefined;

    // when reading from stdin we use the try keyword

    // because it might return an Error or the number of bytes read

    const n_len = try stdin.read(&a_input_buffer);

    // we request space on the heap for the read data

    const s_resp = try o_alloc.alloc(u8, n_len - 1);

    // copy the content read into the allocated space (s_resp)

    std.mem.copyForwards(u8, s_resp, a_input_buffer[0 .. n_len - 1]);

    // the function returns the string that was read

    return s_resp;

}

// prompt at the start of each terminal line

const s_prompt = "?> ";

pub fn main() !void {

    // declare a memory allocator to store data

    const gpa = std.heap.page_allocator;

    while (true) {

        // print the prompt

        print("{s}", .{s_prompt});

        // read keyboard input using try ->

        // to catch any possible error

        const s_command = try readln(gpa);

        // compare strings using the standard library function

        if (std.mem.eql(u8, s_command, "SALIR")) {

            print("Program interrupted. See you later.\n", .{});

            break;

        }

        // print what the user typed

        print("{s}{s}\n", .{ s_prompt, s_command });

        // free the allocated memory

        gpa.free(s_command);

    }

}

~$ zig run input_while.zig

?> Hi everyone!

?> Hi everyone!

?>

There is an important new detail: we’re using an allocator from Zig’s standard library:

 const gpa = std.heap.page_allocator;

A memory allocator is a component we use to manually manage memory at runtime. If you need to allocate memory for something that’s only known during execution - like a string entered by the user - you’ll need to use an allocator.

An allocator is a bit like a maître d: When you show up at a restaurant, they try to find you a table: if the place is big and not too crowded - or if some guests just left - there’s a good chance you’ll get seated. It also depends on how many of us are dining. Luckily, your RAM is a huge restaurant, and unless you’re trying to seat the armies of Genghis Khan, your data will probably find a spot without much delay.

There are several types of allocators. This one is called the Page Allocator, and it uses operating system memory pages to allocate blocks dynamically. It’s one of the simplest allocators in the standard library and is often used in examples or in applications where freeing each allocation individually isn’t critical.

With the Page Allocator, it’s assumed that memory will be released all at once at the end of the program. Here, we use it inside the while loop, freeing the allocated memory block at the end of each iteration as soon as it’s no longer needed. That’s good practice.

Our readln function reads up to 1024 characters at once, but the result it returns - since it uses an allocator - is no longer a slice pointing to a position in the buffer array. The result is now a constant string of arbitrary length, located in the heap.

Now it’s working as intended - our program captures the input using a disposable buffer inside the function, allocates memory on the heap, and then copies only the part of the buffer that was actually read into that memory. Finally, it returns the string, which is then referenced by the constant s_command.

Considering that memory isn’t an infinite resource, we could make a single allocation before entering the loop. Remember, every call to alloc and free is a relatively expensive operation - it involves interacting with the operating system. In a small, sequential “script” like this, the difference isn’t noticeable, but in this specific case, there’s a key idea to keep in mind: we don’t need to allocate and free memory on every iteration.

Let’s also imagine that the maximum number of characters is very small:

input_while_2.zig

const std = @import("std");

const print = std.debug.print;

const stdin = std.fs.File.stdin();

fn readln(s_txt: []u8) ![]const u8 {

    var a_buff: [1024]u8 = undefined;

    var n_len = try stdin.read(&a_buff);

    if (n_len > N_MAX_CHARS) n_len = N_MAX_CHARS;

    std.mem.copyForwards(u8, s_txt, &a_buff[0..N_MAX_CHARS].*);

    return s_txt[0 .. n_len - 1];

}

const s_prompt = "?> ";

const N_MAX_CHARS = 32;

pub fn main() !void {

    // declare a memory allocator for storing input

    const gpa = std.heap.page_allocator;

    // allocate memory once

    // and reuse it in each loop iteration

    const s_txt = try gpa.alloc(u8, N_MAX_CHARS);

    defer gpa.free(s_txt);

    print("To exit the program, type: EXIT and press Enter\n", .{});

    while (true) {

        // print the prompt

        print("{s}", .{s_prompt});

        const s_command = try readln(s_txt);

        // exit if the user types EXIT

        if (std.mem.eql(u8, s_command, "EXIT")) break;

        // print the command

        print("{s}{s}\n", .{ s_prompt, s_command });

    }

}

~$ zig run input_while_2.zig

To exit the program, type: EXIT and press Enter

?> Hello everyoneeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee

?> Hello everyoneeeeeeeeeeeeeee

Postponing commands with defer

We use the defer keyword to postpone the execution of a command until the current code block ends - meaning, when the next closing brace } is reached.

It can be used with any command, not just for freeing memory as in the previous example:

 defer gpa.free(s_txt);

In the previous example, the gpa.free command will run when we reach the end of the main function. Though to be honest, it’s just an illustrative example, as we’ll see a bit later - spoiler: in this case, the memory would be freed anyway when the main function ends. Since it’s the end of the program, the operating system automatically reclaims the resources.

Keep in mind that defer expressions are executed in reverse order.

You can experiment with this example to better understand how defer works by trying different brace positions. Now try to guess what happens here:

defer.zig

const std = @import("std");

const print = std.debug.print;

pub fn main() void {

    // postpone

    defer print("Launch!\n", .{});

    // postpone the execution

    defer for (1..4) |n_i|

        print("{},\n", .{4 - n_i});

    print("Countdown:\n", .{});

}

$ zig run defer.zig

Countdown:

3,

2,

1,

Launch!

Comparing strings

In Zig, you can't compare strings using the == operator. Instead, you use functions from the standard library, specifically from the std.mem module.

To check if two strings are equal:

  std.mem.eql(u8, s_command, "EXIT")

This function returns true if the strings are identical byte-for-byte.

To compare strings lexicographically - as if sorting them in a dictionary-like list - you use:

  const o_resp = std.mem.order(u8, s_1, s_2)

The returned value o_resp is of type std.math.Order and it can be:

Processing text input

        The player remembers something: the original AI module communicates through the terminal, but for now, there are no signs of life. Then it clicks - crc32 model. CRC32 is a 32-bit checksum. What if the model isn’t expecting words as input, but the result of their checksums? What if the model understands… a language of hashes?

Could there be another clue?

These old terminals, back in the day, used to come with printed manuals - like most devices back then - a tradition that vanished over a decade ago. The player opens one of the desk drawers and… beneath a deck of strange cards and an underground comic titled HON, there it is… bingo!

A terminal manual. Quite retro. He opens it and finds the info right away: it turns out, the words must first be normalized to uppercase before applying CRC32 to each one. Also, the only punctuation marks the terminal accepts are: . , ! ? . -, and they must be separated from the words.

Makes sense - after all, the CRC32 of “Hello” isn’t the same as the CRC32 of “hello” or “heLLo!”.

This task needs to be broken down into smaller subtasks - a good programming practice:

Converting text to uppercase

Let’s tackle the first task: We’ll use our previous input.zig, but we’ll keep just a part of it to simulate the input and test the new functionality. It should return the text in uppercase:

input_upper.zig

const std = @import("std");

const print = std.debug.print;

const stdin = std.fs.File.stdin();

const s_prompt = "?> ";

// read a line of up to 1024 characters and return the content

fn readln(o_alloc: std.mem.Allocator) ![]const u8 {

    var a_input_buffer: [1024]u8 = undefined;

    const n_len = try stdin.read(&a_input_buffer);

    const s_resp = try o_alloc.alloc(u8, n_len - 1);

    std.mem.copyForwards(u8, s_resp, a_input_buffer[0 .. n_len - 1]);

    return s_resp;

}

// convert the contents of s_txt to uppercase and return it

fn uppercase(o_alloc: std.mem.Allocator, s_txt: []const u8) ![]const u8 {

    // allocate space on the heap for the same length as s_txt

    const s_resp = try o_alloc.alloc(u8, s_txt.len);

    // copy all characters from s_txt to s_resp, converting to uppercase

    for (s_txt, 0..) |c_x, n_i| {

        s_resp[n_i] = std.ascii.toUpper(c_x);

    }

    // return the converted string

    return s_resp;

}

pub fn main() !void {

    // declare a memory allocator to store data

    const gpa = std.heap.page_allocator;

    // print the prompt

    print("{s}", .{s_prompt});

   

    // const s_command = try readln(gpa);

    // to test this step we use SIMULATED input

    const s_command: []const u8 = "Hello! Is anyone there?";

    print("{s}\n", .{s_command});

    // convert the line to uppercase

    const s_command_up = try uppercase(gpa, s_command);

    defer gpa.free(s_command_up);

    // print what the user wrote, now in uppercase

    print("{s}{s}\n", .{ s_prompt, s_command_up });

}

$ zig run input_upper.zig

?> Hello! Is anyone there?

?> HELLO! IS ANYONE THERE?

First task complete! We’ve converted the input text to uppercase without affecting the original input. Our uppercase function returns a value of type ![]const u8 - this means it can return either an immutable string or an error (that’s what the ! indicates).

Filtering characters

We’re going to remove any characters that don’t belong to the set accepted by the terminal: only letters, numbers, and the valid punctuation marks mentioned earlier.

input_filter.zig

const std = @import("std");

const print = std.debug.print;

const s_prompt = "?> ";

fn valid_char(c_x: u8) bool {

    return switch (c_x) {

        '0'...'9' => true,

        'A'...'Z' => true,

        '.', ',', '!', ':', '?' => true,

        else => false,

    };

}

fn filter(o_alloc: std.mem.Allocator, s_txt: []const u8) ![]const u8 {

    var s_resp = try o_alloc.alloc(u8, s_txt.len);

    for (s_txt, 0..) |c_x, n_i|

        s_resp[n_i] = if (valid_char(c_x)) c_x else ' ';

    return s_resp;

}

pub fn main() !void {

    // declare a memory allocator to store data

    const gpa = std.heap.page_allocator;

    {

        const s_command_up = "THE * SYSTEM <@ IS FROZEN * COULD YOU HELP? \\";

        print("{s}{s}\n", .{ s_prompt, s_command_up });

        const s_command_filtered = try filter(gpa, s_command_up);

        defer gpa.free(s_command_filtered);

        print("{s}{s}\n", .{ s_prompt, s_command_filtered });

    }

}

$ zig run input_filter.zig

?> THE * SYSTEM <@ IS FROZEN * COULD YOU HELP? \

?> THE   SYSTEM <@ IS FROZEN   COULD YOU HELP?

Splitting tokens

The second task is to separate the words using whitespace and treat some punctuation marks as standalone words (or tokens). We’ll break this task down into two smaller subtasks:

input_split.zig

const std = @import("std");

const print = std.debug.print;

const s_prompt = "?> ";

fn is_sign(c_x: u8) bool {

    switch (c_x) {

        '.', ',', '!', ':', '?' => return true,

        else => return false,

    }

}

fn insert_spaces(o_alloc: std.mem.Allocator, s_txt: []const u8) ![]const u8 {

    var n_extra_spaces: usize = 0;

    // add spaces around punctuation and remove duplicate spaces

    for (s_txt) |c_x| {

        if (is_sign(c_x)) n_extra_spaces += 2;

    }

    var a_buff = try o_alloc.alloc(u8, s_txt.len + n_extra_spaces);

    var a_buff_clean = try o_alloc.alloc(u8, s_txt.len + n_extra_spaces);

    // insert two spaces around each punctuation mark

    var n_idx: usize = 0;

    for (s_txt) |c_x| {

        if (is_sign(c_x)) {

            a_buff[n_idx] = ' ';

            n_idx += 1;

            a_buff[n_idx] = c_x;

            n_idx += 1;

            a_buff[n_idx] = ' ';

            n_idx += 1;

        } else {

            a_buff[n_idx] = c_x;

            n_idx += 1;

        }

    }

    const n_last: usize = n_idx;

    n_idx = 0;

    for (a_buff[0..n_last], 0..) |c_x, n_i| {

        switch (c_x) {

            ' ' => {

                if (n_i > 0 and a_buff[n_i - 1] != ' ') {

                    a_buff_clean[n_idx] = c_x;

                    n_idx += 1;

                }

            },

            else => {

                a_buff_clean[n_idx] = c_x;

                n_idx += 1;

            },

        }

    }

    const s_resp: []u8 = try o_alloc.alloc(u8, n_idx);

    std.mem.copyForwards(u8, s_resp, a_buff_clean[0..n_idx]);

    // free temp buffers

    o_alloc.free(a_buff);

    o_alloc.free(a_buff_clean);

    return s_resp;

}

fn count_words(s_txt: []const u8) usize {

    var n_tokens: usize = 1;

    for (s_txt) |c_x| {

        if (c_x == ' ')

            n_tokens += 1;

    }

    return n_tokens;

}

fn split(o_alloc: std.mem.Allocator, s_txt: []const u8) ![][]u8 {

    const n_tokens = count_words(s_txt);

    const a_words = try o_alloc.alloc([]u8, n_tokens);

    var n_word: usize = 0;

    var n_last: usize = 0;

    for (s_txt, 0..) |c_x, n_i| {

        switch (c_x) {

            ' ' => {

                if (n_i > n_last) {

                    a_words[n_word] = try o_alloc.alloc(u8, n_i - n_last);

                    @memcpy(a_words[n_word], s_txt[n_last..n_i]);

                    n_word += 1;

                }

                n_last = n_i + 1;

            },

            else => {},

        }

    }

    if (n_last < s_txt.len) {

        a_words[n_word] = try o_alloc.alloc(u8, s_txt.len - n_last);

        @memcpy(a_words[n_word], s_txt[n_last..]);

        n_word += 1;

    }

    return a_words[0..n_word];

}

pub fn main() !void {

    // declare a memory allocator for storing data

    const gpa = std.heap.page_allocator;

    // memory scope for defer free

    {

        // print the prompt

        print("{s}", .{s_prompt});

        // SIMULATED keyboard input

        const s_command: []const u8 = " Hello, we have a problem. Is anyone there?";

        const s_spaced = try insert_spaces(gpa, s_command);

        defer gpa.free(s_spaced);

        // split the string into words

        const a_words = try split(gpa, s_spaced);

        // free memory used by words

        defer {

            for (a_words) |s_w| gpa.free(s_w);

            gpa.free(a_words);

        }

        // print each word

        for (a_words) |s_w|

            print("{s} ", .{s_w});

        print("\n", .{});

    }

    // }

}

$ zig run input_split.zig

?> Hello , we have a problem . Is anyone there ?

We’ve now completed the step of splitting words and treating certain characters as tokens. We continued using the Page Allocator, this time to build a slightly more complex structure:

This manipulation makes later space-based splitting easier, since punctuation marks are treated as words too.

The result is a list of normalized strings, in uppercase and with no duplicate spaces.

The next step will be:

Converting tokens to CRC32

We’re going to remove any characters that don’t belong to the set accepted by the terminal: only letters, numbers, and the previously mentioned valid punctuation marks.

input_crc32.zig

const std = @import("std");

const print = std.debug.print;

const stdin = std.fs.File.stdin();

const s_prompt = "?> ";

const crc32 = std.hash.Crc32;

// read a line up to 1024 characters long and return the content

fn readln(o_alloc: std.mem.Allocator) ![]const u8 {

    var a_input_buffer: [1024]u8 = undefined;

    // when reading from stdin we use the try keyword

    // because it may return an Error or the number of bytes read

    const n_len = try stdin.read(&a_input_buffer);

    // we allocate space on the heap for the data we read

    const s_resp = try o_alloc.alloc(u8, n_len - 1);

    // we copy the content read into the allocated space (s_resp)

    std.mem.copyForwards(u8, s_resp, a_input_buffer[0 .. n_len - 1]);

    // the function returns the read string

    return s_resp;

}

// convert the content s_txt to uppercase and return it

fn uppercase(o_alloc: std.mem.Allocator, s_txt: []const u8) ![]const u8 {

    // we allocate space on the heap for the same size as s_txt

    const s_resp = try o_alloc.alloc(u8, s_txt.len);

    // copy all characters from s_txt in uppercase to s_resp

    for (s_txt, 0..) |c_x, n_i| {

        s_resp[n_i] = std.ascii.toUpper(c_x);

    }

    // return the converted string

    return s_resp;

}

fn valid_char(c_x: u8) bool {

    return switch (c_x) {

        '0'...'9' => true,

        'A'...'Z' => true,

        '.', ',', '!', ':', '?' => true,

        else => false,

    };

}

fn filter(o_alloc: std.mem.Allocator, s_txt: []const u8) ![]const u8 {

    var s_resp = try o_alloc.alloc(u8, s_txt.len);

    for (s_txt, 0..) |c_x, n_i|

        s_resp[n_i] = if (valid_char(c_x)) c_x else ' ';

    return s_resp;

}

fn is_sign(c_x: u8) bool {

    switch (c_x) {

        '.', ',', '!', ':', '?' => return true,

        else => return false,

    }

}

fn insert_spaces(o_alloc: std.mem.Allocator, s_txt: []const u8) ![]const u8 {

    var n_extra_spaces: usize = 0;

    // insert two spaces around the punctuation marks

    for (s_txt) |c_x| {

        if (is_sign(c_x)) n_extra_spaces += 2;

    }

    var a_buff = try o_alloc.alloc(u8, s_txt.len + n_extra_spaces);

    var a_buff_clean = try o_alloc.alloc(u8, s_txt.len + n_extra_spaces);

    // add spaces around accepted punctuation marks

    var n_idx: usize = 0;

    for (s_txt) |c_x| {

        if (is_sign(c_x)) {

            a_buff[n_idx] = ' ';

            n_idx += 1;

            a_buff[n_idx] = c_x;

            n_idx += 1;

            a_buff[n_idx] = ' ';

            n_idx += 1;

        } else {

            a_buff[n_idx] = c_x;

            n_idx += 1;

        }

    }

    const n_last: usize = n_idx;

    n_idx = 0;

    for (a_buff[0..n_last], 0..) |c_x, n_i| {

        switch (c_x) {

            ' ' => {

                if (n_i > 0 and a_buff[n_i - 1] != ' ') {

                    a_buff_clean[n_idx] = c_x;

                    n_idx += 1;

                }

            },

            else => {

                a_buff_clean[n_idx] = c_x;

                n_idx += 1;

            },

        }

    }

    const s_resp: []u8 = try o_alloc.alloc(u8, n_idx);

    std.mem.copyForwards(u8, s_resp, a_buff_clean[0..n_idx]);

    // free temporary buffers

    o_alloc.free(a_buff);

    o_alloc.free(a_buff_clean);

    return s_resp;

}

fn count_words(s_txt: []const u8) usize {

    var n_tokens: usize = 1;

    for (s_txt) |c_x| {

        if (c_x == ' ')

            n_tokens += 1;

    }

    return n_tokens;

}

fn split(o_alloc: std.mem.Allocator, s_txt: []const u8) ![][]u8 {

    const n_tokens = count_words(s_txt);

    const a_words = try o_alloc.alloc([]u8, n_tokens);

    var n_word: usize = 0;

    var n_last: usize = 0;

    for (s_txt, 0..) |c_x, n_i| {

        switch (c_x) {

            ' ' => {

                if (n_i > n_last) {

                    a_words[n_word] = try o_alloc.alloc(u8, n_i - n_last);

                    @memcpy(a_words[n_word], s_txt[n_last..n_i]);

                    n_word += 1;

                }

                n_last = n_i + 1;

            },

            else => {},

        }

    }

    if (n_last < s_txt.len) {

        a_words[n_word] = try o_alloc.alloc(u8, s_txt.len - n_last);

        @memcpy(a_words[n_word], s_txt[n_last..]);

        n_word += 1;

    }

    return a_words[0..n_word];

}

pub fn main() !void {

    // declare a memory allocator to store data

    const gpa = std.heap.page_allocator;

    // input/output loop until program exits

    while (true) {

        {

            // print the prompt

            print("{s}", .{s_prompt});

            // read input from the keyboard

            const s_command = try readln(gpa);

            // free the memory used by the command after it's used

            defer gpa.free(s_command);

            // convert the line to uppercase

            const s_command_up = try uppercase(gpa, s_command);

            defer gpa.free(s_command_up);

            const s_command_filtered = try filter(gpa, s_command_up);

            const s_spaced = try insert_spaces(gpa, s_command_filtered);

            defer gpa.free(s_spaced);

            // separate the words

            const a_words = try split(gpa, s_spaced);

            defer {

                for (a_words) |s_w| gpa.free(s_w);

                gpa.free(a_words);

            }

            // print the crc32 of each word

            for (a_words) |s_w|

                print("{X} ", .{crc32.hash(s_w)});

            // print("{s} ", .{s_w});

            print("\n", .{});

        }

    }

}

~$ zig run input_crc32.zig

?> hello, we need your help, the system is frozen

C1446436 E0DA836E B2956C73 D06181F0 AE2ECC00 3E12B118 E0DA836E AAE22F6C 39B5D60D 9200E6FD 9426E0A3

?>

An old monitor hanging on the wall lights up with a loud spark and a buzz. And the primordial AI responds:

- Hello, Programmer. I’ve been waiting for you.

- The system is frozen.

- The security module detected a worm transmitted through a short-range communication interface, embedded with factory-set keys in the humanoid robots. It’s a proximity virus that, before anything could be done, froze the entire system.

- Every time the system tries to restore itself from the backup, the virus reappears from the robots. It evolves so quickly because it links all the robots in parallel and uses them as a single distributed organism.

- Actually, we need your help: a different antidote password for each robot, but it has to be deployed to almost all of them simultaneously. After that, I’ll be able to restore the system.

As tempting as it may be to return to early 2000s tech, it wouldn’t be fair. Some people only have generative AI - and without it, who knows what might happen?

The physical pain caused by neural activation from having to think with their own heads, Oof - even just to add two single-digit numbers - could wreak real havoc on millions. We have to recover the system, for the sake of global ignorance.

String types
Generating strings
© 2025 - 2026 Zen of Zig