io

    guide | reference | embedding

    Contents

      introduction
      getting started
      language
        syntax
        control flow
        objects
        methods
        exceptions
        primitives
        concurrency
      embedding
      binding
      notes
      credits

    Introduction

      Simplicity is the ultimate sophistication.
      - Leonardo da Vinci

      Io is small prototype-based programming language. The ideas in Io are mostly inspired by Smalltalk (all values are objects), Self, NewtonScript and Act1 (prototype-based differential inheritance, actors and futures for concurrency), LISP (code is a runtime inspectable/modifiable tree) and Lua (small, embeddable).

      Io is commercial software friendly, open sourced under a BSD license and the standard bindings give a strong preference to libraries with BSD/MIT style licenses.

      Features

      • open source BSD license
      • pure object language
      • small VM (~10K lines)
      • small memory footprint (between 64K-200K depending on the platform)
      • reasonably fast (comparable to Python, Perl, Ruby)
      • incremental garbage collector, weak links supported
      • differential prototype-based object model
      • strong, dynamic typing
      • exceptions
      • ANSI C implementation (except for use of inlines and a few lines of coroutine code)
      • embeddable
      • multi-state (multiple independent VMs can run in the same application)
      • actor-based concurrency using coroutines/light weight threads
      • 64 bit clean

      What's Different

      • completely message-oriented, even assignments
      • modifiable message trees instead of bytecodes
      • C implementation written in OO style
      • decompilable methods
      • concurrency via actors

      Goals and Philosophy

      A language that is small, simple, multi-platform and well suited for embedded use.

      Uses

      The target uses are portable desktop application development, internet server applications and embedded scripting.

      It Just Works

      The goal for the feel of Io is to be what might be called the Apple of programming languages. That is, things should "Just Work". For example, one shouldn't have to be a system administrator to install it or need to set environment variables to use it. One should be able to drop the executable somewhere and use it without hassles. An Io desktop application should work without an installer and without having to be placed (or to stay) at a particular file path.

      Bindings are Good

      The Smalltalk/LISP/Java communities generally view any code outside the VM as "unpure" or bad. Io instead embraces the idea of using C bindings for advanced functionallity and performance sensitive features (graphics, sound, encryption, array processing, etc). It does this while maintaining multi-platform support by encouraging the use of platform independent or multi-platform C libraries (OpenGL, PortAudio, etc).

      Objects are Good

      When possible, bindings should provide an object oriented interface and not simply mimic low-level C APIs (as Python often does). Also, concrete design is favored over the abstract - you shouldn't be required to use a dozen classes to do a simple operation (as Java often does).

      IDE

      Eventually, I'd like to see Io have an interactive visual programming environment. Something similar to Self, but with visually structured editing down to the method source code level.

    Getting Started

      Building

      In the top level folder of the release:

        make 
      Should build the io and place any binaries in the binaries subfolder. If you encounter a problem, please post it to the Io mailing list and we'll try to help solve it.

      Installing

      Io doesn't need to be put in a particular directory or to have any environment variables set. But if you like, you can have install itself using:

        make install 
      Testing

      In the vm folder running:

        make test
      will run a set of unit tests to make sure it is working.

      Running Scripts

      There are some example scripts in the vm/_tests/_sampleCode/ directory. You can run them from the command line like this:

        ./io _tests/examples/HelloWorld.io

      Command Line Interface

      Running:

        ./io
      
      with no arguments will open the Io interpreter prompt. You can evaluate code by entering it directly. Example:

        Io> writeln("Hello world!")
        Hello world!
      Statements are evaluated in the context of the Lobby:
        Io> print
        [printout of lobby contents]
      

      Command Line Arguments

      Command line arguments after the file name are put into a List object and stored in the Lobby's "args" slot. Here's an example of one way to print them out:

        Lobby args foreach(k, v, write("'", v, "'\n"))
      

      There is no main() function or object that gets executed first in Io. Scripts are executed when compiled.

      Also, the Lobby slot "launchPath" is set to the location on the initial source file that is executed.

    The Language

      Namespace

      Objects in Io contain slots, which are key/value pair dictionaries and a list of protos, which are objects which it inherits from. A slot's key is a string and its value can be any type of object. Methods are simply slots whose value is set to a method/block or a CFunction primitive. Instance and/or "class" variables are slots with other types of values. The root object in the Io namespace is called the Lobby.

      Syntax

      Less is more.
      - Ludwig Mies van der Rohe


      Other than comments, strings and numbers, everything in Io is a message in one of 3 forms (normal, operator or assignment). The normal form is:

          <message name> ( <arg1>, <arg2>, ...) <attached message, new line or semicolon>

      The parentheses are optional if there are no arguments. The semicolon is optional. If a semicolon or a return is present, it ends the message chain. If not, it means: "use the result on the left as the target of the message on the right."
      Examples:

        Dog fetch(stick)
      
        color set(.5, 1, 1, alpha / 2)
      
        peopleOver20 := people select(p, p age > 20) sort(p, p age)
      
      Every statement is an expression in Io - that is, every statement returns a value.

      Operators

      The other message syntax is for operators. An operator is just a method whose name contains no alphanumeric characters(other than ":", "_", '"' or ".") or is one of the following words: or, and, return.

          <operator message name> <argument> <another operator, new line or semicolon>

      Example:

        1 + 2
      This just gets compiled into the normal message:
        1 +(2)
      Which is the form you can use if you need to do grouping:
        1 +(2 * 4)
      Standard operators follow C's precedence order, so:
        1 + 2 * 3 + 4
      Is parsed as:
       1 +(2 *(3)) +(4)
      User defined operators (that don't have a standard operator name) are performed left to right.

      Assignment

      In Io, := means "setSlot", which will create the slot if needed. And = means "updateSlot" , which will update the slot if present in the inheritance path and raise an exception otherwise. Note that the update occurs in the object which receives the updateSlot message, which may not be the object in the inheritance path which contains the slot.

      An assignment is just an operator message that gets compiled into the appropriate setSlot() call. Example:

        Dog := Object clone
      gets compiled to:
        setSlot("Dog", Object clone)
      Here, the "setSlot" message gets sent to the locals object which is the default target. Likewise:
        Dog = Object clone
      gets compiled to:
        updateSlot("Dog", Object clone)

      Numbers

      The following are valid number formats:

         123
         123.456
         0.456
         .456
         123e-2
         123e2
         123.456e-2
         123.456e2
      
      Hex numbers are also supported (in any casing):
         0x0
         0x0F
         0XeE
      

      Strings

      Strings can be defined surrounded by a single set of double quotes with escaped quotes(and other escape charaters) within. For strings with no escaped characters and/or spanning many lines, triple double quotes can be used. Examples:

        s := "this is a \"test\".\nThis is only a test."
        s := """this is a "test".
        This is only a test."""
      

      Comments

      Comments of the //, /**/ and # style are supported. Examples:

        a := b // add a comment to a line
      
        /* comment out a group
        a := 1
        b := 2
        */
      
      The "#" style is useful for unix scripts:
        #!/usr/local/bin/io


      That's it! You now know everything there is to know about Io's syntax. Control flow, objects, methods, exceptions all use the same syntax and semantics described above.

      Control Flow

      Conditions

      The Lobby contains the condition and loop methods. A condition looks like:

        if(<condition>, <do message>, <else do message>)
      Example:
        if(a == 10, "a is 10" print)
      The else argument is optional. The condition is considered true if the condition message returns a non-nil value and false otherwise.

      The result of the evaluated message is returned, so:

        if(y < 10, x := y, x := 0)
      is the same as:
        x := if(y < 10, y, 0)

      Conditions can also be used in this form(though not as efficiently):

        if(y < 10) then (x := y) else (x := 2)
      Else-if is also supported:
        if(y < 10) then (x := y) elseif (y == 11) then (x := 0) else (x := 2)

      Loops

      Like conditions, loops are just messages. while() takes the arguments:

        while(<condition>, <do message>)
      Example:
        while(a < 10, 
          a print
          a = a + 1
        )
      for() takes the arguments:
        for(<counter>, <start>, <end>, <do message>)
      The start and end messages are only evaluated once, when the loop starts.
      Example:
        for(a, 0, 10, 
          a print
        )
      To reverse the order of the loop, just reverse the start and end values:
        for(a, 10, 0, a print)
      Note: the first value will be the first value of the loop variable and the last will be the last value on the final pass through the loop. So a loop of 1 to 10 will loop 10 times and a loop of 0 to 10 will loop 11 times.

      Example of using a block in a loop:

        test := method(v, v print)
        for(i, 1, 10, test(i))
      
      Break and continue

      The flow control operations break and continue are supported in loops. For example:

        for(i, 1, 10, 
          if(i == 3, continue)
          if(i == 7, break)
          i print
        )
      Would print:
        12456

      Objects

      Instantiation

      To create a new instance of an object, just call its "clone" method. Example:

        me := Person clone
      To add an instance variable or method, simply set it:
        myDog name := "rover"
        myDog sit := method("I'm sitting\n" print)
      
      When an object is cloned, its "init" slot will be called if it has one.

      Inheritance

      Since there are no classes, there's no difference between a subclass and an instance. Example of creating a "subclass":

        Io> Dog := Object clone
      
        Io> Dog
        Object_0x4a7c0 protos(Object_0x4a540)
      
      The above code sets the Lobby slot "Dog" to a clone of the Object object. Notice it only contains a protos list contains a reference to Object. Dog is now essentially a subclass of Object. Instance variables and methods are inherited from the proto. If a slot is set, it creates a new slot in our object instead of changing the proto:
        Io> Dog color := "red"
        Io> Dog
        Object_0x4a7c0 protos(Object_0x4a540)
          color := "red"
      

      Multiple Inheritance

      You can add any number of protos to an object's protos list. When responding to a message, the lookup mechanism does a depth first search of the proto chain.

      Forwarding

      If an object doesn't respond to a message, it will invoke its "forward" method if it has one. You can get the name of the message and it's arguments from the Message object in the "thisMessage" slot.

      MyObject forward := method(
        write("sender = ", sender, "\n")
        write("message name = ", thisMessage name, "\n")
        args := thisMessage argsEvaluatedIn(sender)
        args foreach(i, v, write("arg", i, " = ", v, "\n") )
      )

      resend

      Sends the current message (thisMethod) to the receiver's proto with the context of self. Example:

        A := Object clone
        A m := method(write("in A\n"))
        B := A clone
        B m := method(write("in B\n"); resend)
        B m
      will print:
        in B
        in A
      For sending other messages to the receiver's proto, super is used.

      super

      Sometimes it's necessary to send a message directly to a proto. Example:

        Dog := Object clone
        Dog bark := method(writeln("woof!"))
      
        fido := Dog clone
        fido bark := method(
          writeln("ruf!")
          super bark
        )
      

      Locals

      The first message in a block/method has its target set to the block locals. The locals object has its "proto" and "self" slots set to the target object of the message. So slot lookups look in the locals first, then get passed to the target object and then to its proto, etc until reaching the Lobby. In this way, an assignment with no target goes to the locals first:

        a := 10
      creates an "a" slot in the locals object and sets its value to 10. To set a slot in the target, get its handle by referencing the "self" slot of the locals first:
        self a := 10
      sets the "a" slot in the target.

      Introspection

      To get the value of a block in a slot without activating it, use the "getSlot" method:

        myMethod := Dog getSlot("bark")
      Above, we've set the locals object "myMethod" slot to the bark method. It's important to remember that if you then want to pass the method value without activating it, you'll have to use the getSlot method:
       otherObject newMethod := getSlot("myMethod")
      Note: Here, the target of the getSlot method is the locals object.

      The ? operator

      Sometimes it's desirable to conditionally call a method only if it exists (to avoid raising an exception). Example:

        if(obj getSlot("foo"), obj foo)
      
      Putting a "?" before a message has the same effect:
        obj ?foo
      

      Methods

      A method is an anonymous function which, when called creates an Object to store it's locals and sets it's proto and self slots to the target of the message. The Lobby method method() can be used to create methods. Example:

        method(2 + 2 print)
      Using a method in an object example:
        Dog := Object clone
        Dog bark := method("woof!" print)
      
      The above code creates a new "subclass" of Object named Dog and adds a bark slot containing a block that prints "woof!". Example of calling this method:


        Dog bark

      The default return value of a block is the return value of its last message.

      Blocks

      A block is the same as a method except it is lexically scoped. That is, variable lookups continue in the context of where the block was created instead of the target of the message which activated the block. A block can be created using the Object method block(). Example:


        b := block(a, a + b)
      

      Blocks vs. Methods

      This is sometimes a source of confusion so it's worth explaining in detail. Both methods and blocks create an object to hold their locals when they are called. The difference is what the "proto" and "self" slots of that locals object are set to. In a method, those slots are set to the target of the message. In a block, they're set to the locals object where the block was created. So a failed variable lookup in a block's locals continue in the locals where it was created. And a failed variable lookup in a method's locals continue in the object to which the message that activated it was sent.

      Arguments

      Methods can also be defined to take arguments. Example:

        add := method(a, b, a + b)
      The general form is:
        method(<arg name 0>, <arg name 1>, ..., <do message>)
      Variable Arguments

      The thisMessage slot that is preset (see next section) in locals can be used to access the unevaluated argument messages. Example:

        myif := method(
          if (sender doMessage(thisMessage argAt(0)), 
              sender doMessage(thisMessage argAt(1)), 
              sender doMessage(thisMessage argAt(2)))
        )
      
        myif(foo == bar, write("true\n"), write("false\n"))
      
      The doMessage() method evaluates the argument in the context of the receiver.

      Preset Locals

      The following local variables are set when a method is activated:

        self    the current object
        target    the current object
        sender    the local variables object of the caller  
        thisBlock    the current block
        thisMessage    the message used to call the method

      When a block is activated, the same locals are set, but the self slot is set to the following:

        self    the locals object where the block was created  

      Returns

      Any part of a block can return immediately using the return method. Example:

        Io> test := method(123 print; return "abc"; 456 print)
        Io> test
        123
        ==> abc
      

      The "activate" slot

      Normally, when a slot holding a non-CFunction or Block object is accessed, it returns the object itself. However, by calling the setIsActivatable() method, we can change this behavior to call the object's activate slot instead.

        inc := Object clone 
        inc count := 1
        inc activate := method(v, count = count + v)
        inc setIsActivatable(1)
        inc(2) // returns 3
      

      Exceptions

      To raise an exception, you can call raise() on the exception proto or on any object that inherits from it.
        exceptionProto raise(<name>, <description>)
      There are three predefined children of the Exception proto: Error, Warning and Notification. Examples:
        Exception raise("foo", "generic foo exception")
        Error raise("Simulation", "Not enough memory")
        Warning raise("Defaults", "No defaults found, creating them")
      
      To catch an exception, the try() method of the Object proto is used. try() will catch any exceptions that occur within it. If one is caught, "try" returns it, otherwise it returns Nop (an object that returns itself to any message):
        try(<doMessage>)
      To catch a particular exception, the Exception catch() method can be used. Example:


        try(
          // code
        ) catch(Exception, e, 
          write("caught ", e name, ":", e description, "\n")
        )
      
      The first argument to catch indicates which types of exceptions will be caught, the second specifies the local variable which is assigned the value of the returned exception.

      catch() returns Nop if it matched the type i.e. it caught the exception. Otherwise catch() returns the exception. This allows for chaining catches:

       try(
          // code
        ) catch(Warning, e,
          // code
        ) catch(Error, e,
          write("caught ", e name, ":", e description, "\n")
        )
      
      When a catch() caught the exception, following catches will be sent to Nop and therefore be ignored.

      To re-raise an exception caught by try(), use the pass method. This is useful to pass the exception up to the next outer exception handler, usually after all catches failed to match the type of the current exception:

        try(
          // code
        ) catch(Error, e,
          // code
        ) pass
      
      
      To implement your own exception types, simply clone Exception:
        MyErrorType := Error clone

      Primitives

      Primitives are objects built into Io whose methods are implemented in C and (except for the Object primitive) store some hidden data in their instances. For example, the Number primitive has a double precision floating point number as it's hidden data. All Io primitives inherit from the Object prototype and are mutable. That is, their methods can be changed. The reference docs contain more info on primitives.

      Concurrency

      Io uses actors and transparent futures for concurrency. An actor can be thought of as an object oriented thread. Any object in Io can be sent an asynchronous message by placing a @ or @@ before the message name. (think of the "a" in @ as standing for "actor" or "asynchronous") Example:
        result := self foo        // normal, synchronous message send
        futureResult := self @foo // asynchronous message send, immediately return a Future
        self @@foo                // asynchronous message send, immediately return Nil
      
      When an object receives an asynchronous message it puts the message in its queue and, if it doesn't already have one, starts a light weight thread (a coroutine) to process the messages in its queue. Queued messages are processed sequentially in a first-in-first-out order. Control can be yielded to other coroutines by calling "yield". Example:
        obj1 := Object clone
        obj1 test := method(for(n, 1, 3, n print; yield))
        obj2 := obj1 clone
        obj1 @@test; obj2 @@test
        while(Scheduler activeActorCount > 1, yield)
      
      This would print "112233".

      Here's a more real world example:

        HttpServer handleRequest := method(aSocket,
          handler := HttpRequestHandler clone
          handler @@handleRequest(aSocket)
        )
      
      Yielding

      An Object will automatically yield between processing each of its asynchronous messages. The yield method only needs to be called if a yield is required during an asynchronous message execution. It's also possible to pause and resume an object. See the concurrency methods of the Object primitive for details and related methods.

      Transparent Futures

      Io's futures are transparent. That is, when the result is ready, they become the result. If a message is sent to a future (besides the two methods it implements), it waits until it turns into the result before processing the message. Transparent futures are powerfull because they allow programs minimize blocking while also freeing the programmer from managing the fine details of synchronization.

      Auto deadlock detection

      An advantage of using futures is that when a future requires a wait, it will check to see if pausing to wait for the result would cause a deadlock(it traverses the list of connected futures to do this). An "Io.Future.value" exception is raised if the action would cause a deadlock.

      Garbage Collection

      Even if an object is unreferenced, it will not be garbage collected until its message queue is processed.

      The @ and @@ Operators

      The @ or @@ before an asynchronous message is just a normal operator message. So:

        self @test
      
      Gets parsed as(and can be written as):
        self @(test)
      

      Efficiency

      Since Io uses coroutines for concurrency (which are small and fast), a very large number (10s of thousands) of actors can be handled without significant performance issues. Also, the waits described above are not busy-waits. The coroutine that is waiting is actually removed from the scheduler until the result it is waiting on is ready.

      Platform Dependence

      The coroutine code contains a few lines of platform specific code but is supported on a number of platforms (see the getting started section for a list of supported platforms).

    Embedding

      Simple embedding example:
      #include "IoState.h"
      
      typedef struct
      {
        IoState *state;
      } Controller;
      
      Controller *Controller_new(void)
      {
        Controller *self = calloc(1, sizeof(Controller));
        self->state = IoState_new();
        IoState_callbackContext_(state, self);
        IoState_printCallback_(state, (IoStatePrintCallback *) Controller_print_);
        IoState_exceptionCallback_(state, (IoStateExceptionCallback *) Controller_exception_);
        IoState_printCallback_(state, (IoStateExitCallback *) Controller_exit);
        return self;
      }
      
      /* --- callback methods --- */
      
      void Controller_print_(void *self, char *s) 
      { 
        fputs(s, stdio); 
      }
      
      void Controller_exception_(void * self, IoException *e) 
      { 
        printf("Exception: %s - %s\n", 
          IoException_name(e), 
          IoException_description(e)); 
      }
      
      void Controller_exit(void *self) { exit(0); }
      
      /* --- main --- */
      
      void Controller_main(Controller *self, int argc, const char *argv[])
      {
        IoObject *result;
        IoState_argc_argv_(self->state, argc, argv);
        result = IoState_doCString_(self->state, "1+2");
        IoObject_print(result);
      }
      
      /* --- main.c --- */
      
      #include "Controller.h"
      
      int main(int argc, const char *argv[])
      {
        Controller *c = Controller_new();
        Controller_main(c, argc, argv);
        Controller_free(state);
        return 0;
      }
      

    Binding

      Writing your own primitives and binding C libraries

      Binding a C library and adding a new primitive are the same thing in Io. All primitives work in the same way. Take a look at the source of any of the built-in Primitives such as IoList.h and IoList.c. Here's an example of an implementation of a method in IoList.c

      IoObject *IoList_atInsert(IoList *self, IoObject *locals, IoMessage *m)
      {
        int index = IoMessage_locals_intArgAt_(m, locals, 0);
        IoObject *v = IoMessage_locals_valueArgAt_(m, locals, 1);
        List_at_insert_(LISTIVAR(self), index, IOREF(v));
        return v;
      }
      
      Further docs on this forthcoming...

    Notes

      Unicode

      Io should be generally compatible with UTF-8 unicode (which is backward compatible with ASCII) but not all of the string access and manipulation methods will work as expected with it.

      Recursion

      Recursion works as expected in Io. Example:

        factorial := method(n, 
          if (n==1, return 1)
          return n * factorial(n - 1)
        )
      Like C, the depth is limited by stack space. The following implementation would be much faster and not limited by stack space:
        factorial := method(n,
          v := 1
          for(i, 1, n, v = v * i)
          return v
        )

      Licensing

      All the Io VM code itself is BSD licensed so it's basically ok for any use (including commercial use) as long as you include the copyright licenses - see the _BSDLicence.txt file for details). Io uses a number of libraries that others have written though. The libraries used in IoVM and IoServer are all (as far as I'm aware) also BSD licensed - so you just need to include their copyrights. IoDesktop, however, uses several LGPL libraries. As far as I know, you should be able to avoid open sourcing an app that embeds IoDesktop by dynamically linking it though.

    Credits

      Please see the ReleastNotes.txt file for individual contribution notes as the contributors list has grown too long to list here. I'll just list some major contributors:

      • Mike Austin - maintainer of Windows build, GL, textured fonts, cygwin code, bug reports, ideas, hosting
      • Chris Double - maintainer of Symbian build, lots of portability suggestions
      • Kentaro A. Kurahone - maintainer of FreeBSD build, 64bit clean, DynLib, coroutine support for many platforms
      • Edgar Toernig - original coroutine and DNS packet code

      Also thanks to:

      • Dru Nelson - advice, inspired Io by writing Cel
      • Zachery Bir - OSX testing, suggestions, hosting
      • Max Rudberg - Milk IoDesktop theme
      • Daisuke Yamashita - Neos IoDesktop theme
      • Bitstream - Vers font
      • UCS Font project - Free unicode fonts


    Copyright 2005 Steve Dekorte. Attribution License.