Skip to content

Implementation Specific Behavior

Branden J Brown edited this page Apr 4, 2020 · 9 revisions

This page describes ways in which this implementation differs from Steve Dekorte's original C implementation of Io. As shorthand on this page, "iolang" means this implementation, and "Io" means the original implementation. (The same rule holds in most places throughout the source code as well.)

Concurrency

Io has three different implementations for the scheduler: Fibers on Windows, ucontext on supported UNIX-derived operating systems, and setjmp everywhere else - none of which are parallel. iolang uses goroutines, leveraging the built-in scheduler in the Go runtime, and true parallelism is allowed.

In Io, only one coroutine is executing at a time, which means that every other coroutine is yielding. This means that the Scheduler yieldingCoros list contains every coroutine other than the current one. One Io programming pattern involves using the size of that list to implement a wait group, yielding until it is smaller than a chosen size.

In iolang, because there is no way to determine which goroutines are currently executing, and because there may be any number of them executing in parallel, all coroutines are considered to be constantly executing unless they explicitly wait on another coroutine (e.g. by requesting the value of a Future). Scheduler yieldingCoros is not a list; instead, it is a Scheduler CFunction which iterates through all active coroutines. It is implemented to provide the same behavior as Io's, i.e. returning every coroutine except the one which called the method. However, it blocks new coroutines from launching or finishing while scanning through the coroutines, so its performance will be very poor in highly concurrent programs.

Programs which depend on the single-execution behavior of Io by using Scheduler yieldingCoros (or Coroutine yieldingCoros) to wait on other coroutines will probably perform poorly in iolang; there is a new Scheduler coroCount method that simulates Scheduler yieldingCoros size much more efficiently. Using something like if(Scheduler hasLocalSlot("coroCount") not, Scheduler coroCount := method(yieldingCoros size)) will make cross-compatibility easier. Any new methods to manipulate the scheduler in Io by modifying yieldingCoros will fail in iolang, because in iolang, yieldingCoros is a CFunction that constructs a list.

Sequence

Sequence has several significant differences. The primary differences regard encoding. String literals are encoded in UTF-8 instead of ASCII. iolang uses UTF-16 and UTF-32 instead of UCS-2 and UCS-4. The Sequence methods asUCS2 and asUCS4 do not exist, and setEncoding will not accept "ucs2" or "ucs4" as an argument. iolang accepts Latin-1 as a (preferred) synonym for ASCII. There is a new asLatin1 method analogous to asUTF8 and friends to convert to Latin-1.

iolang's sequences generally have worse performance in both memory usage and speed of operations than Io's. Io uses type punning of the underlying data in Sequence objects for efficient conversions between item types. iolang avoids this due to stronger typing in Go, which means that converting to a new data type involves copying the entire sequence. Additionally, when converting to a larger item type, iolang will discard any bytes that don't fit into a complete element. "abc" asMutable setItemType("uint16") size will be 2 in Io (reading past the end of the allocated array) but 1 in iolang. (I might eventually change iolang's behavior in this regard to match Io's, zero-padding the data as needed to convert the entire buffer.)

In iolang, converting a Sequence to a string is a particularly expensive operation if the encoding is not UTF-8. Most methods that treat the Sequence as a string, including asString and anything that involves "characters" rather than "elements," convert to string internally. If you plan to use more than a few of the methods implemented in sequence-string.go (initSequence() in sequence.go organizes methods by file), it's a good idea to convert to UTF-8 as early as possible.

Some Sequence methods are not implemented. Instead of the four ordered comparison operators, iolang implements compare, which the Object implementations of <, >=, &c. use. The translate method is not implemented, because I can't figure out what it's supposed to do for multi-byte item types and the Io interpreter segfaults when I try it. The cPrint method is not implemented because it seems to be the same as Object print.

Aside from the above, some Sequence methods have slightly different behavior:

  • pack and unpack treat the s format without a size prefix as indicating a nul-terminated string. Io instead encodes a string of length 1.
  • dotProduct does not require its receiver to be mutable. Io's does.
  • meanSquare performs all its calculations in type float64. Io squares the sequence values in the sequence's type, then converts to float64 before summing.
  • normalize requires its receiver to be numeric. Io's does not.
  • lastPathComponent removes trailing slashes, returns . for the empty string, and returns the system's path separator for a string of only slashes. Io in these cases respectively preserves trailing slashes, returns the empty string, and returns the same number of slashes.
  • pathComponent has the same differences as lastPathComponent, plus it treats a trailing slash as a new path element. For "a/b/c/" pathComponent, iolang returns a/b/c, but Io returns a/b.

No Proxies

Io implements "proxies" as objects that "become" other objects by replacing the object at the proxy's address. This is possible and (relatively) safe because Io does not evaluate messages in parallel. In iolang, changing an object's type indicator is subject to dangerous races, where one parallel coroutine executing a CFunction might verify an object's type, another coroutine causes the object to become a different type, and then the CFunction executes. Or, the type indicator could change during the type check, which would cause unpredictable behavior. Rather than synchronizing every CFunction activation, iolang does not provide true proxies. This has a few consequences that hopefully won't impact programs:

  • Future objects remain Future objects forever. Once the promised value is available, the Future's Activate method activates that instead, and its forward slot delegates to the value. This means that the getSlot method will return the Future itself, which in turn means that activatable objects (usually just methods) inside Futures can't be accessed other than to activate them. Additionally, using getSlot("slotContainingAFuture") type will always produce "Future".
  • There is no FutureProxy type.
  • There is no Object become method.

Date

The Date methods zone and convertToZone don't exist; iolang provides convertToLocation using location names that Go's time.LoadLocation accepts (e.g. "America/New_York"), and location that returns either "Local" or "UTC", instead.

cpuSecondsToRun measures real time, not CPU time.

Minor Differences

The differences below are unlikely to cause issues with real-world programs.

Numeric Literals

iolang's lexer behaves differently with certain forms of numeric literals:

  • Io parses 0xabcdefghi as a single hexadecimal number with the value 0xabcdef. The ghi component remains in the message text, but it is ignored for the message's cachedResult. iolang parses the same text as a hexadecimal token 0xabcdef and an identifier token ghi.
  • Io parses 1234. as an identifier. iolang parses it as a number with 1234 as its value.
  • Io parses 1234e as an identifier. iolang produces an error with this form.
  • Io parses 1234x as a number 1234 and an identifier x. iolang produces an error with this form.

Object

  • - calls its argument's negate method in iolang, so that it doesn't depend on its type. In Io, the argument must be Number.
  • There is a new asGoRepr method, primarily for debugging.
  • continue accepts an argument in iolang, which replaces the loop's evaluation result if continue causes the loop to exit. In Io, any argument to continue is ignored, and if continue causes the loop to exit, then the last loop result is returned. (Or it should; for(x, 0, 2, continue) seems to return the current coroutine.)
  • shallowCopy makes a shallow copy of the receiver's protos in iolang. In Io, the result has no protos.
Clone this wiki locally