Exploring Retro on iOS (continued)

Going slightly further than yesterday, I've implemented code to capture the output and display it below the editor. It still needs a lot of work, but is now functional on both the simulator and my iPad Mini. (I've tested 11.4 - 11.6 images with it).

I'll likely try to build a UI similar to Apologue for this, though some things will be different due to the underlying VM differences.

For those curious, I'm using a modified version of libretro.c with a thin ViewController for UI. I've removed the TTY setup in the rxPrepareOutput() and replaced the character display with code writing to a character array. The ViewController copies out the latest changes and adds them to the output area, then resets the output buffer. This is done every second or so. The stack display is updated after an evaluation completes.

I think that I'll clean up the ViewController a bit and add a tabbed controller so that additional screens will be more readily doable. And then I'll work to flesh out functionality to make it more useful.

Exploring Retro on iOS

This is all pretty early, but I'm making a little headway on getting Retro running under iOS. As of today I can:

  • load the retroImage
  • evaluate strings from an editor window
  • display resulting stack contents

Now to capture output and display it, and then find better ways to handle input...

Apologue 1.0

I'm pleased to announce that Apologue is now available on the iOS App Store for $1.99 (with a temporary launch price of $0.99 this weekend). 

Apologue is an iOS-based programming environment for the Parable language. It provides a code editor, results display, memory browser, documentation, and some useful language tweaks.

A few notes:

  • Apologue runs code on a remote server, so a network connection is required
  • A single evaluation will consume around 300-500K of network data
  • The backend uses the Python implementation of Parable
  • Apologue provides two language tweaks
    • Support numbers without the # prefix
    • Support definitions spanning multiple lines (requiring a blank line after each definition)
  • Requires iOS 7.1 or newer
  • iPad only

I am planning to roll out updates to this on a regular basis, keeping it in sync with the latest Parable changes. I also have plans for adding several new things over the next couple of months including management of multiple projects, code sharing, a decompiler, and some editor improvements.

Interfacing with Retro: Interpreting A String

Considering how to handle implementing an interpreter for my experiments using the retroImage directly, I've decided to try pairing Retro with a modified version of the Parable parser. Below is the first stab at this. It effectively ties basic support of numbers (# prefix) and standard execution-via-class-handlers.

Compilation won't work correctly yet, and there's no string or other prefixes. But this looks like it'll work out, so I'll flesh it out more tonight and see if I can't get it to a usable state.

def interpret(str, d, memory, stack, inputs):
    nest = []
    cleaned = ' '.join(str.split())
    tokens = cleaned.split(' ')
    count = len(tokens)
    i = 0
    offset = 0
    while i < count:
        s = ""
        if tokens[i].startswith("#"):
            stack.append(int(tokens[i][1:]))
        else:
            try:
                executeWithClass(lookup(tokens[i], d), stack, memory, inputs)
            except:
                pass
        i += 1
    return

Interfacing with Retro: Executing Functions While Preserving the Stack

When I left off, I had finished an initial function for executing a single function. This worked, but didn't allow me to carry the stack contents between runs. It also suffered from not supporting class handlers, which are one of the major features of Retro.

So going forward, I needed to fix both of these. I implemented a new routine (executeWithClass) which takes a tuple containing an xt and a class, as well as the stack, memory, and i/o handlers.

Constructing the tuple is handled by a small helper function:

def lookup(name, d):
    return d[name]['xt'], d[name]['class']

The executeWithClass is pretty straightfoward. Instead of creating a new stack, we pass one in along with the tuple returned by lookup.

def executeWithClass( pair, stack, memory, inputs ):
  global EXIT
  ip = pair[1]
  stack.append(pair[0])
  EXIT = len( memory )
  address = [] * 1024
  ports = [0] * 12
  files = [0] * 8

  while ip < EXIT:
    opcode = memory[ip]

    # There are 31 opcodes ( 0 .. 30 ).
    # Instructions above this range are treated as implicit calls.

    if opcode > 30:
      address.append( ip )
      ip = memory[ip] - 1
      try:
        while memory[ip + 1] == 0:
          ip += 1
      except IndexError: pass

    else:

      if   opcode ==  0:   # nop
        pass

      elif opcode ==  1:   # lit
        ip += 1
        stack.append( memory[ip] )

      elif opcode ==  2:   # dup
        stack.append( stack[-1] )

      elif opcode ==  3:   # drop
        stack.pop()

      elif opcode ==  4:   # swap
        a = stack[-2]
        stack[-2] = stack[-1]
        stack[-1] = a

      elif opcode ==  5:   # push
        address.append( stack.pop() )

      elif opcode ==  6:   # pop
        stack.append( address.pop() )

      elif opcode ==  7:   # loop
        stack[-1] -= 1
        if stack[-1] != 0 and stack[-1] > -1:
          ip += 1
          ip = memory[ip] - 1
        else:
          ip += 1
          stack.pop()

      elif opcode ==  8:   # jump
        ip += 1
        ip = memory[ip] - 1
        if memory[ip + 1] == 0:
          ip += 1
          if memory[ip + 1] == 0:
            ip += 1

      elif opcode ==  9:   # return
        ip = address.pop()
        if memory[ip + 1] == 0:
          ip += 1
          if memory[ip + 1] == 0:
            ip += 1

      elif opcode == 10:   # >= jump
        ip += 1
        a = stack.pop()
        b = stack.pop()
        if b > a:
          ip = memory[ip] - 1

      elif opcode == 11:   # <= jump
        ip += 1
        a = stack.pop()
        b = stack.pop()
        if b < a:
          ip = memory[ip] - 1

      elif opcode == 12:   # != jump
        ip += 1
        a = stack.pop()
        b = stack.pop()
        if b != a:
          ip = memory[ip] - 1

      elif opcode == 13:   # == jump
        ip += 1
        a = stack.pop()
        b = stack.pop()
        if b == a:
          ip = memory[ip] - 1

      elif opcode == 14:   # @
        stack[-1] = memory[stack[-1]]

      elif opcode == 15:   # !
        mi = stack.pop()
        memory[mi] = stack.pop()

      elif opcode == 16:   # +
        t = stack.pop()
        stack[ -1 ] += t
        stack[-1] = unpack('=l', pack('=L', stack[-1] & 0xffffffff))[0]

      elif opcode == 17:   # -
        t = stack.pop()
        stack[-1] -= t
        stack[-1] = unpack('=l', pack('=L', stack[-1] & 0xffffffff))[0]

      elif opcode == 18:   # *
        t = stack.pop()
        stack[-1] *= t
        stack[-1] = unpack('=l', pack('=L', stack[-1] & 0xffffffff))[0]

      elif opcode == 19:   # /mod
        a = stack[-1]
        b = stack[-2]
        stack[-1], stack[-2] = rxDivMod( b, a )
        stack[-1] = unpack('=l', pack('=L', stack[-1] & 0xffffffff))[0]
        stack[-2] = unpack('=l', pack('=L', stack[-2] & 0xffffffff))[0]

      elif opcode == 20:   # and
        t = stack.pop()
        stack[-1] &= t

      elif opcode == 21:   # or
        t = stack.pop()
        stack[-1] |= t

      elif opcode == 22:   # xor
        t = stack.pop()
        stack[-1] ^= t

      elif opcode == 23:   # <<
        t = stack.pop()
        stack[-1] <<= t

      elif opcode == 24:   # >>
        t = stack.pop()
        stack[-1] >>= t

      elif opcode == 25:   # 0;
        if stack[-1] == 0:
          stack.pop()
          ip = address.pop()

      elif opcode == 26:   # inc
        stack[-1] += 1

      elif opcode == 27:   # dec
        stack[-1] -= 1

      elif opcode == 28:   # in
        t = stack[-1]
        stack[-1] = ports[t]
        ports[t] = 0

      elif opcode == 29:   # out
        pi = stack.pop()
        ports[ pi ] = stack.pop()

      elif opcode == 30:   # wait
        # Only call if we have pending I/O
        if ports[0] == 0:
          ip = rxHandleDevices( ip, stack, address, ports, memory, files, inputs )

    ip += 1
  return stack, address, memory

And now it's functional. Things like this are now possible:

  d = populate_dictionary(memory)
  stack = [] * 128
  stack.append(1)
  stack.append(2)
  try:
    executeWithClass(lookup('+', d), stack, memory, inputs)
  except:
    pass
  try:
    executeWithClass(lookup('putn', d), stack, memory, inputs)
  except:
    pass

I think the next thing to do is to implement a parser and build an interpreter loop. And then refine the functions I've written into a usable target.

Interfacing with Retro: Running Functions Directly

Previously I posted some code for constructing a Python dictionary that provided access to the named functions in a retroImage. The next step is being able to run an arbitrary function apart from the listener loop. 

I have achieved this through a dedicated processing loop. The loop is a minor variant on the process loop in retro.py. It has been extended to take a function pointer to start at. With this function, it's now possible to directly execute a function or two:

  d = populate_dictionary(memory)
  try:
      processFunction(d['words']['xt'], memory, inputs)
  except:
      pass

The processFunction routine is currently defined as:

def processFunction( xt, memory, inputs ):
  ip = xt
  global EXIT
  EXIT = len( memory )
  stack = [] * 128
  address = [] * 1024
  ports = [0] * 12
  files = [0] * 8

  while ip < EXIT:
    opcode = memory[ip]

    # There are 31 opcodes ( 0 .. 30 ).
    # Instructions above this range are treated as implicit calls.

    if opcode > 30:
      address.append( ip )
      ip = memory[ip] - 1
      try:
        while memory[ip + 1] == 0:
          ip += 1
      except IndexError: pass

    else:

      if   opcode ==  0:   # nop
        pass

      elif opcode ==  1:   # lit
        ip += 1
        stack.append( memory[ip] )

      elif opcode ==  2:   # dup
        stack.append( stack[-1] )

      elif opcode ==  3:   # drop
        stack.pop()

      elif opcode ==  4:   # swap
        a = stack[-2]
        stack[-2] = stack[-1]
        stack[-1] = a

      elif opcode ==  5:   # push
        address.append( stack.pop() )

      elif opcode ==  6:   # pop
        stack.append( address.pop() )

      elif opcode ==  7:   # loop
        stack[-1] -= 1
        if stack[-1] != 0 and stack[-1] > -1:
          ip += 1
          ip = memory[ip] - 1
        else:
          ip += 1
          stack.pop()

      elif opcode ==  8:   # jump
        ip += 1
        ip = memory[ip] - 1
        if memory[ip + 1] == 0:
          ip += 1
          if memory[ip + 1] == 0:
            ip += 1

      elif opcode ==  9:   # return
        ip = address.pop()
        if memory[ip + 1] == 0:
          ip += 1
          if memory[ip + 1] == 0:
            ip += 1

      elif opcode == 10:   # >= jump
        ip += 1
        a = stack.pop()
        b = stack.pop()
        if b > a:
          ip = memory[ip] - 1

      elif opcode == 11:   # <= jump
        ip += 1
        a = stack.pop()
        b = stack.pop()
        if b < a:
          ip = memory[ip] - 1

      elif opcode == 12:   # != jump
        ip += 1
        a = stack.pop()
        b = stack.pop()
        if b != a:
          ip = memory[ip] - 1

      elif opcode == 13:   # == jump
        ip += 1
        a = stack.pop()
        b = stack.pop()
        if b == a:
          ip = memory[ip] - 1

      elif opcode == 14:   # @
        stack[-1] = memory[stack[-1]]

      elif opcode == 15:   # !
        mi = stack.pop()
        memory[mi] = stack.pop()

      elif opcode == 16:   # +
        t = stack.pop()
        stack[ -1 ] += t
        stack[-1] = unpack('=l', pack('=L', stack[-1] & 0xffffffff))[0]

      elif opcode == 17:   # -
        t = stack.pop()
        stack[-1] -= t
        stack[-1] = unpack('=l', pack('=L', stack[-1] & 0xffffffff))[0]

      elif opcode == 18:   # *
        t = stack.pop()
        stack[-1] *= t
        stack[-1] = unpack('=l', pack('=L', stack[-1] & 0xffffffff))[0]

      elif opcode == 19:   # /mod
        a = stack[-1]
        b = stack[-2]
        stack[-1], stack[-2] = rxDivMod( b, a )
        stack[-1] = unpack('=l', pack('=L', stack[-1] & 0xffffffff))[0]
        stack[-2] = unpack('=l', pack('=L', stack[-2] & 0xffffffff))[0]

      elif opcode == 20:   # and
        t = stack.pop()
        stack[-1] &= t

      elif opcode == 21:   # or
        t = stack.pop()
        stack[-1] |= t

      elif opcode == 22:   # xor
        t = stack.pop()
        stack[-1] ^= t

      elif opcode == 23:   # <<
        t = stack.pop()
        stack[-1] <<= t

      elif opcode == 24:   # >>
        t = stack.pop()
        stack[-1] >>= t

      elif opcode == 25:   # 0;
        if stack[-1] == 0:
          stack.pop()
          ip = address.pop()

      elif opcode == 26:   # inc
        stack[-1] += 1

      elif opcode == 27:   # dec
        stack[-1] -= 1

      elif opcode == 28:   # in
        t = stack[-1]
        stack[-1] = ports[t]
        ports[t] = 0

      elif opcode == 29:   # out
        pi = stack.pop()
        ports[ pi ] = stack.pop()

      elif opcode == 30:   # wait
        # Only call if we have pending I/O
        if ports[0] == 0:
          ip = rxHandleDevices( ip, stack, address, ports, memory, files, inputs )

    ip += 1
  return stack, address, memory

The next steps will be the ability to persist the data stack between calls and execute functions via their associated class handlers. This should open things up quite a bit more. With these it'll be feasible to implement a high level token-by-token interpreter routine, though handling the compiler and Retro's parsing/input routines will require a bit more work.