5 August 2025

Universal Turing Machine

Turing Machine in Bletchley Park (licensed CC BY-NC-ND by Douglas Hoyt)The more I practice, the more I explore extreme situations. I have tried to go faster. Other times I Programmed with Nothing. Maybe it is the difficulty of only using low level constructs that makes these group of coding constraints appealing to me - which makes coding in assembly still interesting to some people, too. Then I tried to create Fizz Buzz using a Turing machine. I guess I lacked the purity of Computer Science university exercises. I will dive into Turing machines from a practical coding perspective using Java here.

Turing Machine
What is a Turing machine (TM)? First the TM is a though experiment. It was not meant to be practical. It is "the thing with the tape where a read-write head is able to read a zero or a one, write a zero or a one, erase the value, and move left or right." It is supposed to be Turing complete, which means it is equivalent to all programming languages today. That was my understanding of a TM when I started. But that understanding was impractical. Is a TM a program or a set of programs or a programming language? How can it be a specific program and Turing complete at the same time? And how could I implement anything like that?

Basic (Java) Building Blocks
First I need the building blocks. There is a set of symbols, maybe only 0 or 1 (a bit) or maybe a character to make things easier. So an empty marker interface Symbol should do. (The interface is optional, I like to make the symbol different from plain Object. I could use a plain char as well.) Then there is the tape which can be read and written to. The Tape class represents the tape as a data structure, such as an array or a list. Each cell of the tape holds a symbol.
class Tape<SYM extends Symbol> {

  private Map<Integer, SYM> cells = new TreeMap<>();
  private SYM defaultSymbol;
  private int headPosition = 0;

  Tape(List<SYM> symbols, SYM defaultValue) {
    for (int i = 0; i < symbols.size(); i++) {
      this.cells.put(i, symbols.get(i));
    }
    this.defaultSymbol = defaultValue;
  }

  SYM read() {
    return cells.getOrDefault(headPosition, defaultSymbol);
  }

  void write(SYM symbol) {
    cells.put(headPosition, symbol);
  }

  void moveHead(Direction direction) {
    switch (direction) {
    case LEFT:
      headPosition--;
      break;
    case NONE:
      break;
    case RIGHT:
      headPosition++;
      break;
    default:
      throw new IllegalArgumentException(direction.name());
    }
  }
}
The tape could be infinite, and I chose a Map for each symbol by its position. The default symbol is used for tape positions that have not been written to. The constructor accepts the initial content of the tape as the starting state of the machine. Now to the Turing machine:
class TuringMachine<SYM extends Symbol> {

  private Tape<SYM> tape;
  private State state;

  TuringMachine(Tape<SYM> tape, State initialState) {
    this.tape = tape;
    this.state = initialState;
  }

  private void setState(State state) {
    this.state = state;
  }

  private SYM read() {
    return tape.read();
  }

  private void write(SYM symbol) {
    tape.write(symbol);
  }

  private void move(Direction direction) {
    tape.moveHead(direction);
  }

  void loop() {
    while (!state.isTerminal()) {
      act();
    }
  }

  // ...
Following the description of a Turing machine, it has a state which can change. It can read the symbol currently present on the tape at the position of its read/write head. It can write a new symbol on the tape at the position of its read/write head, replacing the symbol that was previously there. And it can move its read/write head one position to the left or right along the tape. The machine runs as long as its state is not final. In each step (method act shown below) the machine reads the symbol present in the tape's cell, and based on the current state and the read symbol, it determines the next state and the symbol to write on the tape, then it moves the head left or right by one cell.
class TuringMachine<SYM extends Symbol> {

  // ...

  private void act() {
    SYM symbol = read();

    // transition to nextState determines
    // symbolToWrite and directionToMove

    setState(nextState);
    write(symbolToWrite);
    move(directionToMove);
  }

}
Of course this was not my initial code. I reached it by refactoring, cleaning up and structuring the code again and again. If this is a Turing machine, where is the actual program? The Tape and TuringMachine classes form a Universal Turing Machine (UTM). The UTM can simulate the behaviour of any Turing machine. It takes the description of any arbitrary Turing machine as input and mimics its operation on a given input string. Aha! My infrastructure can run an arbitrary Turing machine which makes each TM an individual program and the UTM the interpreter. Maybe I should have read the whole article on Wikipedia first.

Turing machine (licensed CC BY by Maria Keays)State Transitions
The actual program of a Turing machine is the set of its states and possible state transitions. It is a state machine, i.e. a graph of state nodes, and each edge between two states contains additional data: The Symbol symbolToWrite and the Direction directionToMove. In text, the easiest way to define this state machine is with a transition table of State state, Symbol symbol, State newState, Symbol newSymbol, Direction direction which is a TransitionTableRow in my TransitionTable implementation. (The whole transition logic is too much code to show here, the working solution is on my GitHub.) As I will create many transitions, I add defaults to the tables, e.g. state == null applies to all states, symbol == null applies to all symbols and newState == null or newSymbol == null does not change the current state or symbol. I want to avoid defining more table rows than strictly necessary.

A Simple Example
Now is the perfect time for an example. We want to set all bits of a binary number, e.g. 011010. The symbols are the bits 0 and 1 and a marker for the end of the input,
enum Bit implements Symbol {
  _0, _1, END_OF_TAPE
}
Starting at the left-most / highest bit. The transitions are
  • If the machine is running and the current bit is 0, then keep running, write a 1 and move right.
  • If the machine is running and the current bit is 1, then keep running, write a 1 and move right.
  • If the machine is running and the input has ended, then stop running (halt).
This is a simple machine and there are only two states: running and stopped,
enum S implements State {
  RUNNING, HALT;

  @Override
  public boolean isTerminal() { return this == HALT; }
}
and the whole things runs in this test,
class SimpleExampleTest {

  @Test
  void replaces0With1() {
    S initialState = S.RUNNING;
    var _011010 = Arrays.asList(_0, _1, _1, _0, _1, _0, END_OF_TAPE);
    var tape = new Tape<>(_011010, END_OF_TAPE);

    TransitionTable transitions = new TransitionTable().
        row(S.RUNNING, _0, null, _1, Direction.RIGHT).
        row(S.RUNNING, _1, null, null, Direction.RIGHT).
        row(S.RUNNING, END_OF_TAPE, S.HALT, null, Direction.NONE);

    var machine = new TuringMachine<>(tape, transitions, initialState);

    machine.loop();

    var expected_111111 = Arrays.asList(_1, _1, _1, _1, _1, _1, END_OF_TAPE);
    var actualResult = tape.getCells();
    assertEquals(expected_111111, actualResult);
  }
}
Now I know what a Turing machine is and how it is supposed to work. Using the Constructive Approach I created an UTM (TuringMachine.java) and an instance of a TM (in SimpleExampleTest.java). Now I can play around and see what is possible using only state transitions... I encourage you to do the same, clone my fizzbuzz-turing-machine repository and play around with the code in the universal package. This concludes the first part - the foundation if you like - of me implementing Fizz Buzz using a Turing machine.

7 July 2025

Only Exceptions for Control Flow

Where It Started
At technical unconferences I like to start (and conclude) the day with some fun coding. After all, writing code is (still, in June 2025,) at the heart of software development. During SoCraTes Linz 2024 I proposed the first session to be a "Coding Fun session: Only use exceptions for control flow." I explained that it would be impractical but fun ;-) Using exceptions like that was an anti pattern but I wanted to see what would be possible.

Coding Fun: Only Exceptions for Control Flow
I start the session with a few slides: Control flow is is the order in which individual statements, instructions or function calls of an imperative program are executed or evaluated. And the idea of the session is to only use exceptions for changing the control flow. This is a constraint, an artificial addition to a coding exercise, to make it more focused or challenging. To force us to use exceptions for control flow I need to take away all other options:
  • Not using if, while or for, the ternary operator or similar language constructs. (This is the same as the constraint Cyclomatic Complexity One.)
  • Not using any array or (Java) stream operations like map, filter, etc. (These functions are a way to deal with Cyclomatic Complexity One.)
  • Not using lookup arrays of functions. For example a map with the keys true and false and functions as values is an if-else statement. (These lookups are another way to deal with Cyclomatic Complexity One.)
  • Not using modulo, signum, minimum and maximum Math functions as well as various String translate and replace functions. (These are unrelated to Cyclomatic Complexity One, but offer ways to work around in the specified task further down.)
control room in former refinery (licensed CC BY-NC-ND by Michal Jancek)Word of Warning
This is an exercise, even more it is an "inverted" exercise because it wants us to do things we usually avoid doing. And for good reason, using exceptions this way is an anti pattern. It is basically a goto statement, not a structured break or continue style go to, but a real nasty one where we can jump across multiple stack frames. It makes code hard to understand. Often performance is bad due to the cost of creating exceptions. Don't do this in your project. Keep in mind that we are just playing...

Assignment
As a task let us create a ROT-13 "Encryption", also known as Caesar-Chiffre. Our requirements are:
  1. Write a function encode to encode a message using ROT-13.
  2. Each letter is replaced with one 13 added in the alphabet.
  3. At the end of the alphabet, it continues at the beginning.
  4. Characters other than letters are not encoded.
  5. Lowercase letters are first changed to uppercase letters.
  6. Umlauts are letter combinations for encoding, e.g. "Ä" becomes "AE" before encoding and so on.
  7. Bonus: Make rotation configurable. Now it is 13.
These are plenty of requirements. In a usual session we manage to finish item 4) which shows all the relevant parts. If we have more time we can of course do more.

Try it Yourself
Below I will show Java code of the most common solution. After you see it, you will not be able to un-see it. If you want to explore the constraint and the exercise on your own, then stop reading now! I encourage you to do that. Best way is to work with difficult constraints is to code a little bit and then step back and verify if all rules still apply.

Hands On!
I run this session as "interactive demo", which means I let participants decide what to do next, or at least I let them propose what to do next and I wait until a proposition is aligned with my plan. I make sure we stay focused on the main goal of the exercise. The whole coding takes 30 to 40 minutes. Further more I (demand to) use TDD and try to make the code readable (as possible). How do I start? I start with a (simple) failing test of course:
public class Rot13Test {

  @Test
  public void empty() {
    assertEquals("", encode(""));
  }

}
To keep things simple I add the production code (that is the function encode) at the top of the test class:
public class Rot13Test {

  public String encode(String message) {
    return "";
  }
The first test established the method with the required signature, see requirement #1. Now I am able to replace letters with one 13 added in the alphabet. A suitable test case is "A" becomes "N", i.e. assertEquals("N", encode("A")). The first challenge is how to check if there are characters in the message? I cannot loop or map them. How about:
public class Rot13Test {

  public String encode(String message) {
    try {
      message.charAt(0);
      return "N";
    } catch (StringIndexOutOfBoundsException empty) {
      return "";
    }
  }
Ok. Next I choose to go for more letters. I could also do that at the end, it depends what people say during the workshop... A suitable test case is "AA" becomes "NN".
public class Rot13Test {

  public String encode(String message) {
    try {
      char first = message.charAt(0);
      String remaining = message.substring(1);
      return encodeChar(first) + encode(remaining);
    } catch (StringIndexOutOfBoundsException empty) {
      return "";
    }
  }

  private String encodeChar(char c) {
    return "N";
  }
The recursion ends when the message is empty. Back to requirement #2. My next test case is "B" becomes "M" and I only need to modify the method encodeChar():
public class Rot13Test {
  ...

  private String encodeChar(char c) {
    int encoded = c + 13;
    return Character.toString((char) encoded);
  }
This works for the letters "A" to "M" (which becomes "Z"). Nice. Requirement #3 deals with letters "N" to "Z", so
public class Rot13Test {
  ...

  private String encodeChar(char c) {
    int encoded = c + 13;
    try {
      // if encoded <= 'Z' then jump
      int tmp = 1 / (encoded / ('Z' + 1));
      encoded -= 26;
    } catch (ArithmeticException smaller) {
      // leave encoded alone
    }
    return Character.toString((char) encoded);
  }
I will explain how that works: If encoded is less or equal to "Z" the logic is already correct. How can I create an exception if a number is less than 'Z' + 1? If encoded is smaller than this limit, the integer division will be zero which will result in a division by zero, which in Java causes an ArithmeticException. If encoded is larger than "Z", the result of the division will be 1, so dividing by it is allowed and the code continues. Now the algorithm works for all letters.

The Sluice Gate (licensed CC BY-NC-ND by Ib Aarmo)Requirement #4 tells us to leave special characters alone. Which are these? A quick look at the 7 bit ASCII table,
 !"#$%&'()*+,-./0123456789:;<=>?
@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
`abcdefghijklmnopqrstuvwxyz{|}~
shows the special characters. There are three ranges which I need to handle. (At the moment there are only two ranges, and I know that lowercase letters will be handled later.) I add a test for several special characters below, i.e. assertEquals("!/?@", encode("!/?@")) and later one above i.e. assertEquals("[_`{~", encode("[_`{~")), always using the first and last character of each range.
public class Rot13Test {
  ...

  private String encodeChar(char c) {
    int encoded = c;
    try {
      skipIfBelow(encoded, 'A');
      skipIfBelow('Z', encoded);
      encoded += 13;

      skipIfBelow(encoded, 'Z' + 1);
      encoded -= 26;
    } catch (ArithmeticException smaller) {
      // leave encoded alone
    }
    return Character.toString((char) encoded);
  }

  private void skipIfBelow(int codePoint, int limit) {
    int tmp = 1 / (codePoint / limit);
  }
By extracting a helper method skipIfBelow() I can check against the limits of the two ranges: Below and above. The basic algorithm is complete. Requirement #5 is more of the same, adding a toUppercase:
public class Rot13Test {
  ...

  private int toUppercase(int codePoint) {
    try {
      skipIfBelow(codePoint, 'a');
      skipIfBelow('z', codePoint);

      return codePoint - 'a' + 'A';

    } catch (ArithmeticException skipped) {
      // leave char alone
      return codePoint;
    }
  }
Control Flow "Library"
I said I try to make the code more readable. This can be done with custom exceptions and extracting the flow control logic into separate "library" functions like skipIfBelow(). During a different run of the session I ended up with
public class Rot13Test {
  ...

  static class SmallerThan extends Exception {
    public SmallerThan(Throwable cause) {
      super(cause);
    }
  }

  private void testIfSmaller(int codePoint, int limit)
               throws SmallerThan {
    try {
      int tmp = 1 / (codePoint / limit);
    } catch (ArithmeticException e) {
      throw new SmallerThan(e);
    }
  }

  static class LargerThan extends Exception {
    public LargerThan(Throwable cause) {
      super(cause);
    }
  }

  private void testIfLarger(int codePoint, int limit)
               throws LargerThan {
    try {
      testIfSmaller(limit, codePoint);
    } catch (SmallerThan e) {
      throw new LargerThan(e);
    }
  }

  static class ReplacedUmlaut extends Exception {
    public final String replacement;

    public ReplacedUmlaut(String replacement) {
      this.replacement = replacement;
    }
  }

  private void replaceUmlaut(int codePoint, int umlaut,
                             String replacement)
               throws ReplacedUmlaut {
    try {
      testIfSmallerThan(codePoint, umlaut);
      testIfLargerThan(codePoint, umlaut);
      throw new ReplacedUmlaut(replacement);
    } catch (SmallerThan | LargerThan notUmlaut) {
    }
  }
This allows me to distinguish different cases in the catch clause, which becomes a switch statement.

Closing Circle
This code is not like the code you see every day and it was definitely not the code you should write, but all participants agreed that they had a lot of fun. Several people said that I had screwed their mind and my long time peer Bernhard Woditschka said that he liked how it forced him to think outside of the box. This is probably because I am exploring negative aspects, i.e. concepts we usually try to avoid. Samuel Ytterbrink agreed saying "I think this session was like balm for the soul, nothing so freeing as being ordered to do something you are not supposed to do."

Using only exceptions for control flow, like naming all identifiers randomly, focuses on unwanted qualities of code, sharpens our awareness and (hopefully) raises our dislike of them. Ant it is a fun exercise, too. Stay tuned for my next Coding Fun session: No Composition Only Inheritance ;-)

25 June 2025

Patching Io Addons

This project is getting out of hand. I just wanted to use Regular Expressions in Io. This is the forth part of me trying different extensions of Io. First I installed addons without native code, then I compiled native code of addons and in the third part I:
  • Checked package.json and build.io for hints about needed third party libraries and headers.
  • Found ported libraries for Windows in GnuWin32, e.g. ReadLine.
  • Compiled Io addons with dependencies.
  • Fixed the undefined reference to 'IoState_registerProtoWithFunc_' error, which occurs using addons created for older versions of Io.
  • Worked around conflicting headers included by IoObject.h or IoState.h.
  • Finally compiled with dependencies on the native code of another addon.
On the way I added minor fixes to several addons, see my (forked) Io repositories and today I cover addons which needed more extensive modifications.

patch (licensed CC BY-NC-ND by Natasha Wheatland)JSON Parsing
Io has "half" JSON support, most objects provide an asJson() method to represent the contained data in JSON. Some addons (like Docio) require Sequence.parseJson() but omit a specific dependency for that and my Io (iobin-​win32-​current.zip) misses this method. These addons are for a newer version of Io, which has not been released (yet?). Writing a JSON parser is a nice exercise and there is already a JSON parser for Io. It is neither an addon nor an Eerie package. Preparing some arbitrary Io code as addon means moving the existing files into the proper folder structure, and adding the files needed to run as addon (i.e. proto and depends files). I even added a test. My Io JSON addon can be cloned directly into the addons folder %IO_HOME%\​lib\​io\​addons.

Missing Io Core Functions
I started changing addon source code to include the JSON parser addon, at the same time I wanted to keep my changes as little as possible. The Io Guide said that if you have a .iorc file in your home folder, it will be eval'ed before the interactive prompt starts.. In my %HOMEDRIVE%%HOMEPATH%\.iorc I added all definitions of missing Io core functions:
false isFalse := method( true )
true isFalse := method( false )

// load parseJson
Sequence hasSlot("parseJson") ifFalse(
    Json
)

false asJson := "false"
true asJson := "true"
nil asJson := "null"
This made me ask myself, what else was added to Io since my Windows binary was built in 2013? I cloned Io and compared the proper tag with master. There were many changes and I filtered out formatting and comments. The final result was a short list of additions and modifications like
  • Addon and AddonLoader extensions to use Eerie.
  • Core tildeExpandsTo() using UserProfile instead of HOME on Windows.
  • Sequence power() and powerEquals().
  • TestRunner run() returning the number of errors.
Docio
With JSON support and Markdown working, I can run Docio. Docio is the documentation generator for Eerie packages. It extracts tagged comments from C and Io code and generates Markdown and HTML documentation like this. To make Docio work, I had to work around several issues:
  1. Docio's repository name is lowercase. To install it as addon the folder name has to be uppercase, as for Kano:
    > cd "%IO_HOME%\lib\io\addons"
    > git clone git@github.com:IoLanguage/docio.git Docio
  2. Docio does not depend on native code, and it has Markdown as its sole dependency.
    > echo Markdown > Docio\depends
    > echo Docio > Docio\protos
  3. Docio loads Eerie eagerly in the first line of Docio.io. It uses Eerie to query its template path. I lack Eerie and the eager loading fails, so I remove that. With Eerie out of the picture, I always have to provide the full path to the template (%IO_HOME%\lib\io\addons\Docio\template). Now I can initialise Docio with io -e "Docio println". Success.
  4. Docio has a starter script in bin/docio which needs a Windows version, i.e. a bin/docio.bat which calls the original starter script,
    io "%IO_HOME%\lib\io\addons\Docio\bin\docio" %*
  5. With the starter script Docio is available as a command line tool. One thing to know is that its help is wrong. It says
    docio package=/path/to/package [template=/path/to/template]
    while it really needs two dashes for each option,
    docio --package=/path/to/package [--template=/path/to/template]
    Both paths need to be absolute, otherwise documentation files appear in strange places.
  6. There is a syntax error in DocsExtractor.io line 65 and following.
  7. Copying of binary resources, i.e. fonts, corrupts the files. You will have to copy them manually. This is because Io's low level C code does fails to set the binary flag when opening files. On Windows the C library functions consider text files to be finished on the first byte 0x1A. I am unable to fix that right now.
After fixing all that, Docio provides help, even inside the Io REPL, which comes handy:
Io> Docio printDocFor("ReadLine")
Binding to GNU readline.
Docio will generate the documentation for any package on the fly which has a package.json with a name field. Bundled addons miss that file, and I create empty ones containing each addon's name for addons where I want to generate documentation. Here is my own Docio with all the fixes.

Socket
Finally I am going for the Socket addon. I was scared of Socket, some sources stated that Io socket support has always been tricky. Socket depends on libevent 2.0.x, an event notification library. Surprisingly autogen.sh, configure and make install compile and link libevent without any problems. Now I have some libevent_*.dll files. This is way too easy.

But of course Socket's C code does not compile. The header sys/​queue.h is not available on Windows. Fortunately libevent comes with its own compat/​sys/​queue.h. Some header files, e.g. IoEvConnection.h, IoEvHttpServer.h and several others need the conditional include:
#include "Socket.h"
#if !defined(_WIN32) || defined(__CYGWIN__)
#include <sys/queue.h>
#else
#include <compat/sys/queue.h>
#endif
#include <event.h>
Further UnixPath.c fails compilation, and I drop it from the list of files to compile. Even if it would compile, it would not work as indicated by the error returned from IoUnixPath for defined(_WIN32) || defined(__CYGWIN__): Sorry, no Unix Domain sockets on Windows MSCRT.

Linking the compiled files produces an undefined reference to 'GetNetworkParams@8', which is part of the Windows GetNetworkParams interface in iphlpapi.dll, as listed on build.io together with ws2_32. It seems that after my exploration of building addons with native dependencies like ReadLine or Markdown, compiling Socket as difficult than that. Here are the important pieces of the steps:
> ...
> cd source
> gcc -fPIC -D _NO_OLDNAMES -D IO_WINSOCK_COMPAT -c ...
> gcc -liovmall -lbasekit -levent -liphlpapi -lws2_32 ^
      -shared -Wl,--out-implib,libIoSocket.dll.a ^
      ... -o libIoSocket.dll
> ...
My fork of Socket contains the required fixes for Windows.

Using the Socket addon on current Ubuntu
In my product development Coderetreat template, I have GitHub actions to verify the sample code. Based on Sandro's installation instructions I first install the required dependencies:
sudo apt-get install libpcre3-dev libevent-dev
(This might be unecessary as GitHub's Ubuntu image has both of them installed.) The current branch of libevent is 2.1.x and the none of the older 2.0.x versions do compile due to wrong dependencies. At the same time, the Linux version of Io contains Socket but needs libevent 2.0.5 specifically. I have no idea if that is the way to fix these kind of issues, but it works, so I link the installed version (2.1.7) as the required (2.0.5) one.
ls -la /usr/lib/x86_64-linux-gnu/libevent*.so.*
sudo ln -s /usr/lib/x86_64-linux-gnu/libevent-2.1.so.7.0.1 /usr/lib/x86_64-linux-gnu/libevent-2.0.so.5
(I leave the ls is the action to show the current version when the GitHub runner changes and there is a newer version of libevent.) After that I download the "latest" version of Io and install it:
wget -q http://iobin.suspended-chord.info/linux/iobin-linux-x64-deb-current.zip
unzip -qq iobin-linux-x64-deb-current.zip
sudo dpkg -i IoLanguage-2013.11.04-Linux-x64.deb
sudo ldconfig
io --version
The Linux version of Io comes with almost all addons pre-compiled, and there is no need for any compilation. Success. Because this version is without Eerie, custom addons have to be installed manually into the System installPrefix folder, e.g. adding Docio
git clone https://github.com/codecop/Docio.git
io -e "System installPrefix println"
sudo mv Docio /usr/local/lib/io/addons/
io -e "Docio println"
Now the runner is ready to execute some tests which is usually done with io ./tests/correctness/run.io. The full GitHub action for Io is here.

Alaska, Frontier Land (licensed CC BY-NC-ND by Clickrbee)Random Facts about Addons
During my exploration I learned more things about Io's addon structure:
  • The protos file does not need to contain the name of the "main" prototype, which is also the name of the addon, and often it does not. I put the name of all (exported) prototypes there to simplify my scripts. Then package.json, the Eerie manifest, does contain all prototypes.
  • In the beginning I though depends was some kind of manifest, listing dependencies. But it is only needed for dependencies of native code, so AddonLoader loads dependent addons before they are used in native code. For non native addons, Io will load whatever is needed when parsing any unknown type name. Till now the only addon I have seen which needs that feature is Regex.
  • When an addon is loaded all its files are evaluated. Only then is it registered as active. This can lead to addon initialisation loops. The initialisation order seems to be by file name. Some addons with complex inter dependencies - like Socket - prefix Io files with A_0, A_1 and so on to ensure ordered initialisation. (This is a bit annoying for tooling as the prototype name usually equals the file name in Io.)
Summary: My Feelings Towards Io
While Io is dead since more than ten years, working with it feels bleeding edge, even more it is outside the frontier. You are at your own, there is no help. Nobody is coming to save you. The latest article I found was written in 2019. There are less than ten (!) active blog posts: Blame it on Io (2006), Io language on Windows (2014), Io Basics (2015) and Io Programming Language (2019) - to list the best ones. There are a handful of Stack Overflow questions and a few repositories on GitHub - which are sometimes incompatible with the "latest" Io. ChatGPT understands the language but fantasises the libraries, so no help from AI neither. I am used to modern languages with a rich ecosystem, e.g. Java, C#, Python and this is an unfamiliar feeling. At the same time it is a refreshing puzzle. Maybe I will come back for a vacation in uncharted territory.